diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e7cca91f6c99..a5410529fcd757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,8 @@ release. -26.4.0
+26.5.0
+26.4.0
26.3.1
26.3.0
26.2.0
diff --git a/README.md b/README.md index dc175eadf5bb6a..45a01ca66fef4a 100644 --- a/README.md +++ b/README.md @@ -319,8 +319,6 @@ For information about the governance of the Node.js project, see **Dario Piotrowicz** <> (he/him) * [deokjinkim](https://github.com/deokjinkim) - **Deokjin Kim** <> (he/him) -* [edsadr](https://github.com/edsadr) - - **Adrian Estrada** <> (he/him) * [ErickWendel](https://github.com/ErickWendel) - **Erick Wendel** <> (he/him) * [Ethan-Arrowood](https://github.com/Ethan-Arrowood) - @@ -513,6 +511,8 @@ For information about the governance of the Node.js project, see **Xu Meng** <> (he/him) * [dnlup](https://github.com/dnlup) - **dnlup** <> +* [edsadr](https://github.com/edsadr) - + **Adrian Estrada** <> (he/him) * [eljefedelrodeodeljefe](https://github.com/eljefedelrodeodeljefe) - **Robert Jefe Lindstaedt** <> * [estliberitas](https://github.com/estliberitas) - diff --git a/benchmark/webstreams/readable-async-iterator.js b/benchmark/webstreams/readable-async-iterator.js index 8cdc4785db3dac..73ff3e1ef2b165 100644 --- a/benchmark/webstreams/readable-async-iterator.js +++ b/benchmark/webstreams/readable-async-iterator.js @@ -6,23 +6,40 @@ const { const bench = common.createBenchmark(main, { n: [1e5], + type: ['normal', 'bytes'], }); -async function main({ n }) { - const rs = new ReadableStream({ - pull: function(controller) { - controller.enqueue(1); - }, - }); +async function main({ n, type }) { + const rs = type === 'bytes' ? + new ReadableStream({ + type: 'bytes', + pull: function(controller) { + controller.enqueue(new Uint8Array(1)); + }, + }) : + new ReadableStream({ + pull: function(controller) { + controller.enqueue(1); + }, + }); let x = 0; bench.start(); - for await (const chunk of rs) { - x += chunk; - if (x > n) { - break; + if (type === 'bytes') { + for await (const chunk of rs) { + x += chunk.byteLength; + if (x > n) { + break; + } + } + } else { + for await (const chunk of rs) { + x += chunk; + if (x > n) { + break; + } } } // Use x to ensure V8 does not optimize away the loop as a noop. diff --git a/common.gypi b/common.gypi index bae09d8a5d6300..214615c3e085d9 100644 --- a/common.gypi +++ b/common.gypi @@ -24,6 +24,7 @@ 'node_module_version%': '', 'node_with_ltcg%': '', 'node_shared_openssl%': 'false', + 'openssl_is_boringssl%': 'false', 'node_tag%': '', 'uv_library%': 'static_library', @@ -41,7 +42,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.24', + 'v8_embedder_string': '-node.25', ##### V8 defaults for Node.js ##### diff --git a/configure.py b/configure.py index 2dc6d72d159cc4..04ffff0db81982 100755 --- a/configure.py +++ b/configure.py @@ -1440,6 +1440,48 @@ def get_gas_version(cc): warn(f'Could not recognize `gas`: {gas_ret}') return '0.0' +def get_openssl_macros(o): + """Extract OpenSSL preprocessor macros from the configured headers.""" + + # Use the C compiler to extract preprocessor macros from OpenSSL headers. + # crypto.h is included because BoringSSL declares OPENSSL_IS_BORINGSSL there. + args = ['-E', '-dM', + '-include', 'openssl/opensslv.h', + '-include', 'openssl/crypto.h', + '-'] + if not options.shared_openssl: + args = ['-I', 'deps/openssl/openssl/include'] + args + elif options.shared_openssl_includes: + args = ['-I', options.shared_openssl_includes] + args + else: + for dir in o['include_dirs']: + args = ['-I', dir] + args + + proc = subprocess.Popen( + shlex.split(CC) + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + with proc: + proc.stdin.write(b'\n') + out = to_utf8(proc.communicate()[0]) + + if proc.returncode != 0: + warn('Failed to extract OpenSSL macros from headers') + return {} + + macros = {} + for line in out.split('\n'): + if line.startswith('#define OPENSSL_'): + parts = line.split() + if len(parts) >= 2: + macro_name = parts[1] + macro_value = parts[2] if len(parts) >= 3 else '1' + macros[macro_name] = macro_value + + return macros + def get_openssl_version(o): """Parse OpenSSL version from opensslv.h header file. @@ -1449,39 +1491,7 @@ def get_openssl_version(o): """ try: - # Use the C compiler to extract preprocessor macros from opensslv.h - args = ['-E', '-dM', '-include', 'openssl/opensslv.h', '-'] - if not options.shared_openssl: - args = ['-I', 'deps/openssl/openssl/include'] + args - elif options.shared_openssl_includes: - args = ['-I', options.shared_openssl_includes] + args - else: - for dir in o['include_dirs']: - args = ['-I', dir] + args - - proc = subprocess.Popen( - shlex.split(CC) + args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - with proc: - proc.stdin.write(b'\n') - out = to_utf8(proc.communicate()[0]) - - if proc.returncode != 0: - warn('Failed to extract OpenSSL version from opensslv.h header') - return 0 - - # Parse the macro definitions - macros = {} - for line in out.split('\n'): - if line.startswith('#define OPENSSL_VERSION_'): - parts = line.split() - if len(parts) >= 3: - macro_name = parts[1] - macro_value = parts[2] - macros[macro_name] = macro_value + macros = get_openssl_macros(o) # Extract version components major = int(macros.get('OPENSSL_VERSION_MAJOR', '0')) @@ -1511,6 +1521,13 @@ def get_openssl_version(o): warn(f'Failed to determine OpenSSL version from header: {e}') return 0 +def get_openssl_is_boringssl(o): + try: + return b('OPENSSL_IS_BORINGSSL' in get_openssl_macros(o)) + except (OSError, ValueError, subprocess.SubprocessError) as e: + warn(f'Failed to determine whether OpenSSL headers are BoringSSL: {e}') + return 'false' + def get_cargo_version(cargo): try: proc = subprocess.Popen(shlex.split(cargo) + ['--version'], @@ -2315,6 +2332,7 @@ def without_ssl_error(option): configure_library('openssl', o) o['variables']['openssl_version'] = get_openssl_version(o) + o['variables']['openssl_is_boringssl'] = get_openssl_is_boringssl(o) def configure_lief(o): if options.without_lief: diff --git a/deps/cares/CMakeLists.txt b/deps/cares/CMakeLists.txt index a2a9c19329e09b..a9ae8fb952b72a 100644 --- a/deps/cares/CMakeLists.txt +++ b/deps/cares/CMakeLists.txt @@ -12,7 +12,7 @@ INCLUDE (CheckCSourceCompiles) INCLUDE (CheckStructHasMember) INCLUDE (CheckLibraryExists) -PROJECT (c-ares LANGUAGES C VERSION "1.34.6" ) +PROJECT (c-ares LANGUAGES C VERSION "1.34.8" ) # Set this version before release SET (CARES_VERSION "${PROJECT_VERSION}") @@ -30,7 +30,7 @@ INCLUDE (GNUInstallDirs) # include this *AFTER* PROJECT(), otherwise paths are w # For example, a version of 4:0:2 would generate output such as: # libname.so -> libname.so.2 # libname.so.2 -> libname.so.2.2.0 -SET (CARES_LIB_VERSIONINFO "21:5:19") +SET (CARES_LIB_VERSIONINFO "21:7:19") OPTION (CARES_STATIC "Build as a static library" OFF) @@ -43,6 +43,7 @@ OPTION (CARES_BUILD_TOOLS "Build tools" OPTION (CARES_SYMBOL_HIDING "Hide private symbols in shared libraries" OFF) OPTION (CARES_THREADS "Build with thread-safety support" ON) OPTION (CARES_COVERAGE "Build for code coverage" OFF) +OPTION (CARES_WERROR "Treat compiler warnings on the c-ares library as errors" OFF) SET (CARES_RANDOM_FILE "/dev/urandom" CACHE STRING "Suitable File / Device Path for entropy, such as /dev/urandom") # Tests require static to be enabled on Windows to be able to access otherwise hidden symbols @@ -54,6 +55,30 @@ ENDIF () INCLUDE (EnableWarnings) +# Optionally treat warnings as errors for the c-ares library itself. This is +# applied per-target (see cares_set_werror() usage) rather than globally so it +# does not affect the test/fuzz harnesses (which carry their own warnings) or +# CMake's own compiler feature checks. Enabled in CI only on toolchains known +# to be free of spurious warnings at our high warning levels. +FUNCTION (cares_set_werror target) + IF (NOT CARES_WERROR) + RETURN () + ENDIF () + IF (MSVC) + TARGET_COMPILE_OPTIONS (${target} PRIVATE /WX) + ELSE () + TARGET_COMPILE_OPTIONS (${target} PRIVATE -Werror) + # Modern libc headers (e.g. glibc ) use the C11 _Generic + # keyword for const-correctness of strchr()/memchr()/etc. Clang flags + # that as a C11 extension under our C90 + -Wpedantic build even though it + # originates in a system header at our call sites. It is not something + # we can fix in our own code, so warn but do not fail on it. + IF (CMAKE_C_COMPILER_ID MATCHES "Clang") + TARGET_COMPILE_OPTIONS (${target} PRIVATE -Wno-error=c11-extensions) + ENDIF () + ENDIF () +ENDFUNCTION () + IF (MSVC) # allow linking against the static runtime library in msvc OPTION (CARES_MSVC_STATIC_RUNTIME "Link against the static runtime library" OFF) diff --git a/deps/cares/README.md b/deps/cares/README.md index 6566c9fe6aa18e..7e818df8fa5b12 100644 --- a/deps/cares/README.md +++ b/deps/cares/README.md @@ -1,7 +1,7 @@ # [![c-ares logo](https://c-ares.org/art/c-ares-logo.svg)](https://c-ares.org/) -[![Build Status](https://api.cirrus-ci.com/github/c-ares/c-ares.svg?branch=main)](https://cirrus-ci.com/github/c-ares/c-ares) -[![Windows Build Status](https://ci.appveyor.com/api/projects/status/aevgc5914tm72pvs/branch/main?svg=true)](https://ci.appveyor.com/project/c-ares/c-ares/branch/main) +[![Build Status](https://github.com/c-ares/c-ares/actions/workflows/ubuntu-latest.yml/badge.svg?branch=main)](https://github.com/c-ares/c-ares/actions/workflows/ubuntu-latest.yml) +[![Windows Build Status](https://github.com/c-ares/c-ares/actions/workflows/windows.yml/badge.svg?branch=main)](https://github.com/c-ares/c-ares/actions/workflows/windows.yml) [![Coverage Status](https://coveralls.io/repos/github/c-ares/c-ares/badge.svg?branch=main)](https://coveralls.io/github/c-ares/c-ares?branch=main) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/291/badge)](https://bestpractices.coreinfrastructure.org/projects/291) [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/c-ares.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:c-ares) diff --git a/deps/cares/RELEASE-NOTES.md b/deps/cares/RELEASE-NOTES.md index ded75001b54fa4..93cc10fc74bd8e 100644 --- a/deps/cares/RELEASE-NOTES.md +++ b/deps/cares/RELEASE-NOTES.md @@ -1,3 +1,113 @@ +## c-ares version 1.34.8 - July 7 2026 + +This is a bugfix release. + +Bugfixes: +* Revert "Mark parameters in callbacks as const" which shipped in 1.34.7. + Changing the parameter types of the `ares_callback`, `ares_host_callback`, + and `ares_nameinfo_callback` function pointer typedefs was an unintended + API break: existing applications with the historical non-const callback + signatures no longer compiled, particularly in C++ where function pointer + types must match exactly. The read-only nature of the callback parameters + is now documented instead. + [PR #1244](https://github.com/c-ares/c-ares/pull/1244) + +Thanks go to these friendly people for their efforts and contributions for this +release: + +* Brad House (@bradh352) + + +## c-ares version 1.34.7 - July 6 2026 + +This is a security release. + +Security: +* CVE-2026-33630. Use-after-free / double-free in c-ares' query-completion + handling, remotely triggerable via ares_getaddrinfo() over TCP. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-6wfj-rwm7-3542 +* CPU-exhaustion denial of service via unbounded DNS name compression pointer + chains. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-pjmc-gx33-gc76 +* Memory-amplification denial of service via unvalidated DNS header record + counts. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-jv8r-gqr9-68wj + +Changes: +* `ares_getaddrinfo()`: handle a NULL node per POSIX and implement + `ARES_AI_PASSIVE`. [PR #1186](https://github.com/c-ares/c-ares/pull/1186) +* Mark parameters in callbacks as const. + [PR #1060](https://github.com/c-ares/c-ares/pull/1060) +* Correct `ARES_CLASS_HESOID` misspelling to `ARES_CLASS_HESIOD`. + [PR #1092](https://github.com/c-ares/c-ares/pull/1092) + +Bugfixes: +* Fix sticky server recovery when all servers have failures. [PR #1192](https://github.com/c-ares/c-ares/pull/1192) +* Fix UDP socket exhaustion regression: retire connections per-connection, not via server failure count. [PR #1197](https://github.com/c-ares/c-ares/pull/1197) +* Guard DNS record binary length overflow. [PR #1168](https://github.com/c-ares/c-ares/pull/1168) +* Prevent integer overflow in allocation size calculations. [PR #1147](https://github.com/c-ares/c-ares/pull/1147) +* Prevent overflow in ares_array allocation size calculations. [PR #1117](https://github.com/c-ares/c-ares/pull/1117) +* Prevent integer overflow in buffer size calculation. [PR #1116](https://github.com/c-ares/c-ares/pull/1116) +* Add overflow checks to ares_buf_ensure_space(). [PR #1094](https://github.com/c-ares/c-ares/pull/1094) +* Skip name compression offsets beyond the 14-bit pointer limit. [PR #1159](https://github.com/c-ares/c-ares/pull/1159) +* ares_dns_parse: reject name compression in RDATA where not permitted (RFC 3597). [PR #1190](https://github.com/c-ares/c-ares/pull/1190) +* ares_dns_parse: reject responses with more than one OPT record (RFC 6891). [PR #1189](https://github.com/c-ares/c-ares/pull/1189) +* Discard oversized UDP datagrams instead of truncating the length frame. [PR #1161](https://github.com/c-ares/c-ares/pull/1161) +* Route numeric config parsing through range-checked ares_str_parse_uint. [PR #1158](https://github.com/c-ares/c-ares/pull/1158) +* Replace atoi-based port parsing with validated helper. [PR #1097](https://github.com/c-ares/c-ares/pull/1097) +* Defer TCP connection error handling until DNS responses are parsed. [PR #1138](https://github.com/c-ares/c-ares/pull/1138) +* Prevent undefined-behavior left shift in ares_calc_query_timeout(). [PR #1151](https://github.com/c-ares/c-ares/pull/1151) +* Use unsigned type for ares_round_up_pow2_u64 to avoid undefined behavior. [PR #1107](https://github.com/c-ares/c-ares/pull/1107) +* Fix undefined behavior in ares_buf_replace() pointer arithmetic. [PR #1099](https://github.com/c-ares/c-ares/pull/1099) +* ares_array: reset offset when array becomes empty. [PR #1165](https://github.com/c-ares/c-ares/pull/1165) +* ares_iface_ips: add ARES_IFACE_IP_NONE zero enum value. [PR #1187](https://github.com/c-ares/c-ares/pull/1187) +* ares_sysconfig_files: recognize AIX netsvc.conf bind4/local4 tokens. [PR #1188](https://github.com/c-ares/c-ares/pull/1188) +* Use safe string construction in Windows sysconfig join path. [PR #1143](https://github.com/c-ares/c-ares/pull/1143) +* Fix zero-length RAW_RR losing type metadata during parsing. [PR #1129](https://github.com/c-ares/c-ares/pull/1129) +* Fix RAW_RR type tostr/fromstr roundtrip mismatch. [PR #1123](https://github.com/c-ares/c-ares/pull/1123) +* Fix NULL dereference for ifa netmask. [PR #1120](https://github.com/c-ares/c-ares/pull/1120) +* adig: fix negated option prefix parsing. [PR #1135](https://github.com/c-ares/c-ares/pull/1135) +* Use UnregisterWaitEx to prevent use-after-free on Win32. [PR #1111](https://github.com/c-ares/c-ares/pull/1111) +* Add NULL check after ares_malloc_zero in Windows UTF8 conversion. [PR #1121](https://github.com/c-ares/c-ares/pull/1121) +* Fix NULL dereference after ares_malloc_zero in Win32 IOCP event add. [PR #1132](https://github.com/c-ares/c-ares/pull/1132) +* Fix microsecond overflow in ares_queue_wait_empty timeout. [PR #1122](https://github.com/c-ares/c-ares/pull/1122) +* Fix NULL dereference in ares_buf_replace() on NULL buf. [PR #1124](https://github.com/c-ares/c-ares/pull/1124) +* Initialize *read_bytes in ares_socket_recvfrom(). [PR #1125](https://github.com/c-ares/c-ares/pull/1125) +* Fix memory leak of binbuf in ares_buf_parse_dns_binstr_int. [PR #1126](https://github.com/c-ares/c-ares/pull/1126) +* Fix memory leak of qcache entry on key allocation failure. [PR #1127](https://github.com/c-ares/c-ares/pull/1127) +* Fix memory leak of buf in ares_dns_multistring_combined() on OOM. [PR #1110](https://github.com/c-ares/c-ares/pull/1110) +* Fix memory leaks on error paths in two functions. [PR #1109](https://github.com/c-ares/c-ares/pull/1109) +* Fix memory leak of buckets in ares_htable_dict_keys() error path. [PR #1108](https://github.com/c-ares/c-ares/pull/1108) +* Fix memory leak of bucket->key in ares_htable_dict_insert error path. [PR #1105](https://github.com/c-ares/c-ares/pull/1105) +* Fix additional memory leaks. [PR #1091](https://github.com/c-ares/c-ares/pull/1091) [PR #1078](https://github.com/c-ares/c-ares/pull/1078) +* Prevent corrupt addrinfo nodes on sockaddr allocation failure. [PR #1112](https://github.com/c-ares/c-ares/pull/1112) +* Fix two logic bugs in DNS cookie handling. [PR #1103](https://github.com/c-ares/c-ares/pull/1103) +* Fix linked list INSERT_BEFORE corruption and array insertdata_first ordering. [PR #1101](https://github.com/c-ares/c-ares/pull/1101) +* Fix HASH_IDX macro missing parentheses. [PR #1128](https://github.com/c-ares/c-ares/pull/1128) +* Fix malloc(0) in ares_htable_all_buckets on empty table. [PR #1131](https://github.com/c-ares/c-ares/pull/1131) +* Fix wrong sizeof in QNX confstr() call truncating domain names. [PR #1130](https://github.com/c-ares/c-ares/pull/1130) +* Clear probe pending flag after timeout. [PR #1059](https://github.com/c-ares/c-ares/pull/1059) +* Fix incorrect check for empty wide string. [PR #1064](https://github.com/c-ares/c-ares/pull/1064) +* Handle strdup failure. [PR #1077](https://github.com/c-ares/c-ares/pull/1077) +* doc: reference ares_free_string(), not ares_free(). [PR #1084](https://github.com/c-ares/c-ares/pull/1084) + +Thanks go to these friendly people for their efforts and contributions for this +release: + +* (@Alb3e3) +* Brad House (@bradh352) +* (@dankmeme01) +* David Hotham (@dimbleby) +* Tom Flynn (@Flynnzaa) +* (@jmestwa-coder) +* (@kodareef5) +* Kaixuan Li (@MarkLee131) +* (@lifenjoiner) +* (@metsw24-max) +* Song Li (@SongTonyLi) +* (@uwezkhan) + + ## c-ares version 1.34.6 - December 8 2025 This is a security release. diff --git a/deps/cares/aminclude_static.am b/deps/cares/aminclude_static.am index 07239563e6a6d2..9a00ba789172eb 100644 --- a/deps/cares/aminclude_static.am +++ b/deps/cares/aminclude_static.am @@ -1,6 +1,6 @@ # aminclude_static.am generated automatically by Autoconf -# from AX_AM_MACROS_STATIC on Mon Dec 8 16:21:41 UTC 2025 +# from AX_AM_MACROS_STATIC on Tue Jul 7 16:14:13 UTC 2026 # Code coverage diff --git a/deps/cares/configure b/deps/cares/configure index f91e2c956862e7..c2318a1c3bf07a 100755 --- a/deps/cares/configure +++ b/deps/cares/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.71 for c-ares 1.34.6. +# Generated by GNU Autoconf 2.71 for c-ares 1.34.8. # # Report bugs to . # @@ -621,8 +621,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='c-ares' PACKAGE_TARNAME='c-ares' -PACKAGE_VERSION='1.34.6' -PACKAGE_STRING='c-ares 1.34.6' +PACKAGE_VERSION='1.34.8' +PACKAGE_STRING='c-ares 1.34.8' PACKAGE_BUGREPORT='c-ares mailing list: http://lists.haxx.se/listinfo/c-ares' PACKAGE_URL='' @@ -1430,7 +1430,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures c-ares 1.34.6 to adapt to many kinds of systems. +\`configure' configures c-ares 1.34.8 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1501,7 +1501,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of c-ares 1.34.6:";; + short | recursive ) echo "Configuration of c-ares 1.34.8:";; esac cat <<\_ACEOF @@ -1647,7 +1647,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -c-ares configure 1.34.6 +c-ares configure 1.34.8 generated by GNU Autoconf 2.71 Copyright (C) 2021 Free Software Foundation, Inc. @@ -2271,7 +2271,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by c-ares $as_me 1.34.6, which was +It was created by c-ares $as_me 1.34.8, which was generated by GNU Autoconf 2.71. Invocation command line was $ $0$ac_configure_args_raw @@ -3245,7 +3245,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu -CARES_VERSION_INFO="21:5:19" +CARES_VERSION_INFO="21:7:19" @@ -6818,7 +6818,7 @@ fi # Define the identity of the package. PACKAGE='c-ares' - VERSION='1.34.6' + VERSION='1.34.8' printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h @@ -28238,7 +28238,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by c-ares $as_me 1.34.6, which was +This file was extended by c-ares $as_me 1.34.8, which was generated by GNU Autoconf 2.71. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -28306,7 +28306,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -c-ares config.status 1.34.6 +c-ares config.status 1.34.8 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" diff --git a/deps/cares/configure.ac b/deps/cares/configure.ac index 744a99be56ab2f..637dfb153b1f72 100644 --- a/deps/cares/configure.ac +++ b/deps/cares/configure.ac @@ -2,10 +2,10 @@ dnl Copyright (C) The c-ares project and its contributors dnl SPDX-License-Identifier: MIT AC_PREREQ([2.69]) -AC_INIT([c-ares], [1.34.6], +AC_INIT([c-ares], [1.34.8], [c-ares mailing list: http://lists.haxx.se/listinfo/c-ares]) -CARES_VERSION_INFO="21:5:19" +CARES_VERSION_INFO="21:7:19" dnl This flag accepts an argument of the form current[:revision[:age]]. So, dnl passing -version-info 3:12:1 sets current to 3, revision to 12, and age to dnl 1. diff --git a/deps/cares/docs/ares_dns_record.3 b/deps/cares/docs/ares_dns_record.3 index 47ca95b057a246..723f9ab4b98c6a 100644 --- a/deps/cares/docs/ares_dns_record.3 +++ b/deps/cares/docs/ares_dns_record.3 @@ -140,7 +140,7 @@ DNS Classes for requests and responses: .B ARES_CLASS_CHAOS - CHAOS .br -.B ARES_CLASS_HESOID +.B ARES_CLASS_HESIOD - Hesoid [Dyer 87] .br .B ARES_CLASS_NONE diff --git a/deps/cares/docs/ares_getaddrinfo.3 b/deps/cares/docs/ares_getaddrinfo.3 index 5544ce20224eba..661381e0926e8c 100644 --- a/deps/cares/docs/ares_getaddrinfo.3 +++ b/deps/cares/docs/ares_getaddrinfo.3 @@ -27,6 +27,19 @@ The and .I service parameters give the hostname and service as NULL-terminated C strings. +Either +.I name +or +.I service +may be NULL, but not both. If +.I name +is NULL, the returned addresses are synthesized from +.IR service : +the wildcard address (0.0.0.0 or ::) is returned when +.B ARES_AI_PASSIVE +is set in +.IR ai_flags , +otherwise the loopback address (127.0.0.1 or ::1) is returned. The .I hints parameter is an @@ -63,6 +76,16 @@ If this option is set .I service field will be treated as a numeric value. .TP 19 +.B ARES_AI_PASSIVE +If this option is set and +.I name +is NULL, the returned addresses will use the wildcard address (0.0.0.0 or ::), +suitable for +.BR bind (2)ing +a socket that will accept connections. If not set and +.I name +is NULL, the loopback address (127.0.0.1 or ::1) is returned instead. +.TP 19 .B ARES_AI_CANONNAME The ares_addrinfo structure will return a canonical names list. .TP 19 diff --git a/deps/cares/docs/ares_gethostbyaddr.3 b/deps/cares/docs/ares_gethostbyaddr.3 index cc4092285c0168..f5b5f0864fcff1 100644 --- a/deps/cares/docs/ares_gethostbyaddr.3 +++ b/deps/cares/docs/ares_gethostbyaddr.3 @@ -80,7 +80,16 @@ points to a containing the name of the host returned by the query. The callback need not and should not attempt to free the memory pointed to by .IR hostent ; -the ares library will free it when the callback returns. If the query +the ares library will free it when the callback returns. The callback +.B must not +modify the +.B struct hostent +or retain a reference to it after returning; it is owned by c-ares and is only +valid for the duration of the callback. The parameter is intentionally not +declared +.B const +solely to preserve API/ABI compatibility with existing applications. +If the query did not complete successfully, .I hostent will be diff --git a/deps/cares/docs/ares_gethostbyname.3 b/deps/cares/docs/ares_gethostbyname.3 index 06d075ca6c5136..40d340343114f1 100644 --- a/deps/cares/docs/ares_gethostbyname.3 +++ b/deps/cares/docs/ares_gethostbyname.3 @@ -88,7 +88,16 @@ points to a containing the name of the host returned by the query. The callback need not and should not attempt to free the memory pointed to by .IR hostent ; -the ares library will free it when the callback returns. If the query +the ares library will free it when the callback returns. The callback +.B must not +modify the +.B struct hostent +or retain a reference to it after returning; it is owned by c-ares and is only +valid for the duration of the callback. The parameter is intentionally not +declared +.B const +solely to preserve API/ABI compatibility with existing applications. +If the query did not complete successfully, .I hostent will be diff --git a/deps/cares/docs/ares_getnameinfo.3 b/deps/cares/docs/ares_getnameinfo.3 index 66b04f9efc11a7..800b57b31f3f4c 100644 --- a/deps/cares/docs/ares_getnameinfo.3 +++ b/deps/cares/docs/ares_getnameinfo.3 @@ -24,7 +24,7 @@ function is defined for protocol-independent address translation. The function is a combination of \fIares_gethostbyaddr(3)\fP and \fIgetservbyport(3)\fP. The function will translate the address either by executing a host query on the name service channel identified by -.IR channel +.IR channel or it will attempt to resolve it locally if possible. The parameters .I sa @@ -68,8 +68,8 @@ A hostname lookup is being requested. A service name lookup is being requested. .PP When the query -is complete or has -failed, the ares library will invoke \fIcallback\fP. Completion or failure of +is complete or has +failed, the ares library will invoke \fIcallback\fP. Completion or failure of the query may happen immediately, or may happen during a later call to \fIares_process(3)\fP, \fIares_destroy(3)\fP or \fIares_cancel(3)\fP. .PP @@ -129,19 +129,31 @@ given request. .PP On successful completion of the query, the callback argument .I node -contains a string representing the hostname (assuming +contains a string representing the hostname (assuming .B ARES_NI_LOOKUPHOST -was specified). Additionally, +was specified). Additionally, .I service contains a string representing the service name (assuming .B ARES_NI_LOOKUPSERVICE was specified). If the query did not complete successfully, or one of the values -was not requested, +was not requested, .I node or .I service -will be +will be .BR NULL . +.PP +The +.I node +and +.I service +strings are owned by the c-ares library and are only valid for the duration of +the callback. The callback +.B must not +modify or free them, and must not retain references to them after returning; +treat them as read-only. The parameters are intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. .SH SEE ALSO .BR ares_process (3), diff --git a/deps/cares/docs/ares_query.3 b/deps/cares/docs/ares_query.3 index 3aa428b00bb813..401d700a44ccd9 100644 --- a/deps/cares/docs/ares_query.3 +++ b/deps/cares/docs/ares_query.3 @@ -146,6 +146,16 @@ or .I abuf will be non-NULL, otherwise they will be NULL. +The +.I abuf +buffer is owned by the c-ares library and is only valid for the duration of +the callback. The callback +.B must not +modify or free it, and must not retain a reference to it after returning; treat +it as read-only. The parameter is intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. + .SH AVAILABILITY \fBares_query_dnsrec(3)\fP was introduced in c-ares 1.28.0. diff --git a/deps/cares/docs/ares_search.3 b/deps/cares/docs/ares_search.3 index 66791b47e908fb..12d1dede09e874 100644 --- a/deps/cares/docs/ares_search.3 +++ b/deps/cares/docs/ares_search.3 @@ -152,6 +152,15 @@ will usually be NULL and will usually be 0, but in some cases an unsuccessful query result may be placed in .IR abuf . +The +.I abuf +buffer is owned by the c-ares library and is only valid for the duration of +the callback. The callback +.B must not +modify or free it, and must not retain a reference to it after returning; treat +it as read-only. The parameter is intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. The \fIares_search_dnsrec(3)\fP function behaves identically to \fIares_search(3)\fP, but takes an initialized and filled DNS record object to diff --git a/deps/cares/docs/ares_send.3 b/deps/cares/docs/ares_send.3 index df3e3bbe4136b0..cf669a001db295 100644 --- a/deps/cares/docs/ares_send.3 +++ b/deps/cares/docs/ares_send.3 @@ -131,6 +131,16 @@ and .IR alen for \fIares_send(3)\fP will be non-NULL. +The +.I abuf +buffer is owned by the c-ares library and is only valid for the duration of +the callback. The callback +.B must not +modify or free it, and must not retain a reference to it after returning; treat +it as read-only. The parameter is intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. + Unless the flag .B ARES_FLAG_NOCHECKRESP was set at channel initialization time, \fIares_send_dnsrec(3)\fP and diff --git a/deps/cares/include/ares.h b/deps/cares/include/ares.h index 7fe3ec78f4e651..1e38463a044a22 100644 --- a/deps/cares/include/ares.h +++ b/deps/cares/include/ares.h @@ -434,6 +434,14 @@ struct ares_addr { /* DNS record parser, writer, and helpers */ #include "ares_dns_record.h" +/* IMPORTANT: the pointer parameters below (abuf, hostent, node, service) are + * intentionally NOT const. Marking them const changes the function-pointer + * type and breaks API/ABI compatibility for existing C and (especially) C++ + * applications that declare their callbacks with the historical non-const + * signature. This was tried in PR #1060 and reverted for that reason. These + * buffers are owned by c-ares and MUST be treated as read-only by the callback + * (do not modify or free them); that contract is documented in the man pages, + * not enforced via const, to preserve compatibility. Do not re-add const. */ typedef void (*ares_callback)(void *arg, int status, int timeouts, unsigned char *abuf, int alen); diff --git a/deps/cares/include/ares_dns_record.h b/deps/cares/include/ares_dns_record.h index cec9f47f63d8f5..1e47a78e143c4a 100644 --- a/deps/cares/include/ares_dns_record.h +++ b/deps/cares/include/ares_dns_record.h @@ -76,7 +76,8 @@ typedef enum { typedef enum { ARES_CLASS_IN = 1, /*!< Internet */ ARES_CLASS_CHAOS = 3, /*!< CHAOS */ - ARES_CLASS_HESOID = 4, /*!< Hesoid [Dyer 87] */ + ARES_CLASS_HESIOD = 4, /*!< Hesiod [Dyer 87] */ + ARES_CLASS_HESOID = 4, /*!< typo from older c-ares version for Hesiod */ ARES_CLASS_NONE = 254, /*!< RFC 2136 */ ARES_CLASS_ANY = 255 /*!< Any class (requests only) */ } ares_dns_class_t; @@ -1092,7 +1093,7 @@ CARES_EXTERN ares_status_t ares_dns_parse(const unsigned char *buf, * * \param[in] dnsrec Pointer to initialized and filled DNS record object. * \param[out] buf Pointer passed by reference to be filled in with with - * DNS message. Must be ares_free()'d by caller. + * DNS message. Must be ares_free_string()'d by caller. * \param[out] buf_len Length of returned buffer containing DNS message. * \return ARES_SUCCESS on success */ diff --git a/deps/cares/include/ares_version.h b/deps/cares/include/ares_version.h index 006029c12dfe91..f836d5f9186d1c 100644 --- a/deps/cares/include/ares_version.h +++ b/deps/cares/include/ares_version.h @@ -32,8 +32,8 @@ #define ARES_VERSION_MAJOR 1 #define ARES_VERSION_MINOR 34 -#define ARES_VERSION_PATCH 6 -#define ARES_VERSION_STR "1.34.6" +#define ARES_VERSION_PATCH 8 +#define ARES_VERSION_STR "1.34.8" /* NOTE: We cannot make the version string a C preprocessor stringify operation * due to assumptions made by integrators that aren't properly using diff --git a/deps/cares/src/lib/CMakeLists.txt b/deps/cares/src/lib/CMakeLists.txt index bdb2690fb673f9..a4c1e73f1bd5ad 100644 --- a/deps/cares/src/lib/CMakeLists.txt +++ b/deps/cares/src/lib/CMakeLists.txt @@ -66,6 +66,8 @@ IF (CARES_SHARED) TARGET_COMPILE_DEFINITIONS (${PROJECT_NAME} PRIVATE HAVE_CONFIG_H=1 CARES_BUILDING_LIBRARY) + cares_set_werror (${PROJECT_NAME}) + TARGET_LINK_LIBRARIES (${PROJECT_NAME} PUBLIC ${CARES_DEPENDENT_LIBS} PRIVATE ${CMAKE_THREAD_LIBS_INIT} @@ -136,6 +138,8 @@ IF (CARES_STATIC) TARGET_COMPILE_DEFINITIONS (${LIBNAME} PRIVATE HAVE_CONFIG_H=1 CARES_BUILDING_LIBRARY) + cares_set_werror (${LIBNAME}) + # Only matters on Windows IF (WIN32 OR CYGWIN) TARGET_COMPILE_DEFINITIONS (${LIBNAME} PUBLIC CARES_STATICLIB) diff --git a/deps/cares/src/lib/Makefile.in b/deps/cares/src/lib/Makefile.in index e92732eaf72c8d..a9630300c0848b 100644 --- a/deps/cares/src/lib/Makefile.in +++ b/deps/cares/src/lib/Makefile.in @@ -15,7 +15,7 @@ @SET_MAKE@ # aminclude_static.am generated automatically by Autoconf -# from AX_AM_MACROS_STATIC on Mon Dec 8 16:21:41 UTC 2025 +# from AX_AM_MACROS_STATIC on Tue Jul 7 16:14:13 UTC 2026 # Copyright (C) The c-ares project and its contributors # SPDX-License-Identifier: MIT diff --git a/deps/cares/src/lib/ares_addrinfo_localhost.c b/deps/cares/src/lib/ares_addrinfo_localhost.c index 2abb0c48a6f601..b65cf48a2f6a75 100644 --- a/deps/cares/src/lib/ares_addrinfo_localhost.c +++ b/deps/cares/src/lib/ares_addrinfo_localhost.c @@ -67,13 +67,8 @@ ares_status_t ares_append_ai_node(int aftype, unsigned short port, struct ares_addrinfo_node **nodes) { struct ares_addrinfo_node *node; - - node = ares_append_addrinfo_node(nodes); - if (!node) { - return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ - } - - memset(node, 0, sizeof(*node)); + struct sockaddr *sa; + socklen_t salen; if (aftype == AF_INET) { struct sockaddr_in *sin = ares_malloc(sizeof(*sin)); @@ -86,14 +81,9 @@ ares_status_t ares_append_ai_node(int aftype, unsigned short port, sin->sin_family = AF_INET; sin->sin_port = htons(port); - node->ai_addr = (struct sockaddr *)sin; - node->ai_family = AF_INET; - node->ai_addrlen = sizeof(*sin); - node->ai_addr = (struct sockaddr *)sin; - node->ai_ttl = (int)ttl; - } - - if (aftype == AF_INET6) { + sa = (struct sockaddr *)sin; + salen = sizeof(*sin); + } else if (aftype == AF_INET6) { struct sockaddr_in6 *sin6 = ares_malloc(sizeof(*sin6)); if (!sin6) { return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ @@ -104,13 +94,23 @@ ares_status_t ares_append_ai_node(int aftype, unsigned short port, sin6->sin6_family = AF_INET6; sin6->sin6_port = htons(port); - node->ai_addr = (struct sockaddr *)sin6; - node->ai_family = AF_INET6; - node->ai_addrlen = sizeof(*sin6); - node->ai_addr = (struct sockaddr *)sin6; - node->ai_ttl = (int)ttl; + sa = (struct sockaddr *)sin6; + salen = sizeof(*sin6); + } else { + return ARES_EFORMERR; + } + + node = ares_append_addrinfo_node(nodes); + if (!node) { + ares_free(sa); /* LCOV_EXCL_LINE: OutOfMemory */ + return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ } + node->ai_addr = sa; + node->ai_family = aftype; + node->ai_addrlen = salen; + node->ai_ttl = (int)ttl; + return ARES_SUCCESS; } diff --git a/deps/cares/src/lib/ares_close_sockets.c b/deps/cares/src/lib/ares_close_sockets.c index 347f43e6fcd88d..7bb72c31ec1b2a 100644 --- a/deps/cares/src/lib/ares_close_sockets.c +++ b/deps/cares/src/lib/ares_close_sockets.c @@ -112,11 +112,11 @@ void ares_check_cleanup_conns(const ares_channel_t *channel) do_cleanup = ARES_TRUE; } - /* If the associated server has failures, close it out. Resetting the - * connection (and specifically the source port number) can help resolve - * situations where packets are being dropped. + /* If the connection has been retired for new queries, close it out once + * idle. Resetting the connection (and specifically the source port + * number) can help resolve situations where packets are being dropped. */ - if (conn->server->consec_failures > 0) { + if (conn->flags & ARES_CONN_FLAG_NONEW) { do_cleanup = ARES_TRUE; } diff --git a/deps/cares/src/lib/ares_conn.h b/deps/cares/src/lib/ares_conn.h index 16ecefdd19fa67..c332de14b135d4 100644 --- a/deps/cares/src/lib/ares_conn.h +++ b/deps/cares/src/lib/ares_conn.h @@ -43,7 +43,13 @@ typedef enum { ARES_CONN_FLAG_TFO = 1 << 1, /*! TCP Fast Open has not yet sent its first packet. Gets unset on first * write to a connection */ - ARES_CONN_FLAG_TFO_INITIAL = 1 << 2 + ARES_CONN_FLAG_TFO_INITIAL = 1 << 2, + /*! Connection has been retired: it must not be handed any NEW queries (e.g. + * it experienced a query timeout, suggesting packets are being dropped on + * it). Its in-flight queries continue to drain and it is cleaned up once + * idle. This is a per-connection signal so that a transient failure does + * not evict otherwise-healthy connections to the same server. */ + ARES_CONN_FLAG_NONEW = 1 << 3 } ares_conn_flags_t; typedef enum { diff --git a/deps/cares/src/lib/ares_cookie.c b/deps/cares/src/lib/ares_cookie.c index 509e12050e0c00..25960db6022fcc 100644 --- a/deps/cares/src/lib/ares_cookie.c +++ b/deps/cares/src/lib/ares_cookie.c @@ -217,7 +217,7 @@ static const unsigned char * static ares_bool_t timeval_is_set(const ares_timeval_t *tv) { - if (tv->sec != 0 && tv->usec != 0) { + if (tv->sec != 0 || tv->usec != 0) { return ARES_TRUE; } return ARES_FALSE; @@ -324,7 +324,7 @@ ares_status_t ares_cookie_apply(ares_dns_record_t *dnsrec, ares_conn_t *conn, if (cookie->state == ARES_COOKIE_UNSUPPORTED) { /* If timer hasn't expired, just delete any possible cookie and return */ if (!timeval_expired(&cookie->unsupported_ts, now, - COOKIE_REGRESSION_TIMEOUT_MS)) { + COOKIE_UNSUPPORTED_TIMEOUT_MS)) { ares_dns_rr_del_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE); return ARES_SUCCESS; } diff --git a/deps/cares/src/lib/ares_getaddrinfo.c b/deps/cares/src/lib/ares_getaddrinfo.c index 6009de36a3d02d..6d9e21af4bb9a1 100644 --- a/deps/cares/src/lib/ares_getaddrinfo.c +++ b/deps/cares/src/lib/ares_getaddrinfo.c @@ -242,6 +242,11 @@ static ares_bool_t fake_addrinfo(const char *name, unsigned short port, ares_status_t status = ARES_SUCCESS; ares_bool_t result = ARES_FALSE; int family = hints->ai_family; + + if (name == NULL) { + return ARES_FALSE; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + if (family == AF_INET || family == AF_INET6 || family == AF_UNSPEC) { /* It only looks like an IP address if it's all numbers and dots. */ size_t numdots = 0; @@ -268,6 +273,7 @@ static ares_bool_t fake_addrinfo(const char *name, unsigned short port, if (result) { status = ares_append_ai_node(AF_INET, port, 0, &addr4, &ai->nodes); if (status != ARES_SUCCESS) { + ares_freeaddrinfo(ai); callback(arg, (int)status, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ return ARES_TRUE; /* LCOV_EXCL_LINE: OutOfMemory */ } @@ -282,6 +288,7 @@ static ares_bool_t fake_addrinfo(const char *name, unsigned short port, if (result) { status = ares_append_ai_node(AF_INET6, port, 0, &addr6, &ai->nodes); if (status != ARES_SUCCESS) { + ares_freeaddrinfo(ai); callback(arg, (int)status, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ return ARES_TRUE; /* LCOV_EXCL_LINE: OutOfMemory */ } @@ -575,21 +582,55 @@ static void host_callback(void *arg, ares_status_t status, size_t timeouts, /* at this point we keep on waiting for the next query to finish */ } -static ares_bool_t numeric_service_to_port(const char *service, - unsigned short *port) +/* Per POSIX getaddrinfo(), when no node/hostname is provided the returned + * addresses are synthesized from the service: the wildcard address when + * ARES_AI_PASSIVE is set (suitable for bind()ing a listening socket), + * otherwise the loopback address (suitable for connect()ing to a peer on the + * local host). */ +static ares_status_t + ares_append_nulladdr(unsigned short port, + const struct ares_addrinfo_hints *hints, + struct ares_addrinfo *ai) { - char *end; - unsigned long val; + int family = hints->ai_family; + ares_bool_t passive = ARES_FALSE; + ares_status_t status; + struct ares_addrinfo_node *node; - errno = 0; - val = strtoul(service, &end, 10); + if (hints->ai_flags & ARES_AI_PASSIVE) { + passive = ARES_TRUE; + } - if (errno == 0 && *end == '\0' && val <= 65535) { - *port = (unsigned short)val; - return ARES_TRUE; + if (family == AF_INET6 || family == AF_UNSPEC) { + struct ares_in6_addr addr6; + memset(&addr6, 0, sizeof(addr6)); /* wildcard "::" */ + if (!passive) { + ares_inet_pton(AF_INET6, "::1", &addr6); + } + status = ares_append_ai_node(AF_INET6, port, 0, &addr6, &ai->nodes); + if (status != ARES_SUCCESS) { + return status; /* LCOV_EXCL_LINE: OutOfMemory */ + } } - return ARES_FALSE; + if (family == AF_INET || family == AF_UNSPEC) { + struct in_addr addr4; + memset(&addr4, 0, sizeof(addr4)); /* wildcard "0.0.0.0" */ + if (!passive) { + ares_inet_pton(AF_INET, "127.0.0.1", &addr4); + } + status = ares_append_ai_node(AF_INET, port, 0, &addr4, &ai->nodes); + if (status != ARES_SUCCESS) { + return status; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + for (node = ai->nodes; node != NULL; node = node->ai_next) { + node->ai_socktype = hints->ai_socktype; + node->ai_protocol = hints->ai_protocol; + } + + return ARES_SUCCESS; } static void ares_getaddrinfo_int(ares_channel_t *channel, const char *name, @@ -616,6 +657,13 @@ static void ares_getaddrinfo_int(ares_channel_t *channel, const char *name, return; } + /* POSIX getaddrinfo() requires that at least one of node (name) or service + * be provided. */ + if (name == NULL && service == NULL) { + callback(arg, ARES_ENOTFOUND, 0, NULL); + return; + } + if (ares_is_onion_domain(name)) { callback(arg, ARES_ENOTFOUND, 0, NULL); return; @@ -623,14 +671,14 @@ static void ares_getaddrinfo_int(ares_channel_t *channel, const char *name, if (service) { if (hints->ai_flags & ARES_AI_NUMERICSERV) { - if (!numeric_service_to_port(service, &port)) { + if (!ares_parse_port(service, &port, ARES_TRUE)) { callback(arg, ARES_ESERVICE, 0, NULL); return; } } else { port = lookup_service(service, 0); if (!port) { - if (!numeric_service_to_port(service, &port)) { + if (!ares_parse_port(service, &port, ARES_TRUE)) { callback(arg, ARES_ESERVICE, 0, NULL); return; } @@ -644,6 +692,19 @@ static void ares_getaddrinfo_int(ares_channel_t *channel, const char *name, return; } + /* No node/hostname was provided (service-only lookup): synthesize wildcard + * or loopback addresses from the resolved port instead of issuing a query. */ + if (name == NULL) { + status = ares_append_nulladdr(port, hints, ai); + if (status != ARES_SUCCESS) { + ares_freeaddrinfo(ai); /* LCOV_EXCL_LINE: OutOfMemory */ + callback(arg, (int)status, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ + return; /* LCOV_EXCL_LINE: OutOfMemory */ + } + callback(arg, ARES_SUCCESS, 0, ai); + return; + } + if (fake_addrinfo(name, port, hints, ai, callback, arg)) { return; } diff --git a/deps/cares/src/lib/ares_gethostbyname.c b/deps/cares/src/lib/ares_gethostbyname.c index d451b4685110ec..640df74cfc36c3 100644 --- a/deps/cares/src/lib/ares_gethostbyname.c +++ b/deps/cares/src/lib/ares_gethostbyname.c @@ -102,6 +102,10 @@ void ares_gethostbyname(ares_channel_t *channel, const char *name, int family, struct ares_addrinfo_hints hints; struct host_query *ghbn_arg; + if (channel == NULL) { + return; + } + if (!callback) { return; } diff --git a/deps/cares/src/lib/ares_hosts_file.c b/deps/cares/src/lib/ares_hosts_file.c index c0fb80766a7e9b..0c8aebaf6bad9d 100644 --- a/deps/cares/src/lib/ares_hosts_file.c +++ b/deps/cares/src/lib/ares_hosts_file.c @@ -674,6 +674,9 @@ static ares_status_t ares_hosts_path(const ares_channel_t *channel, &dwLength); ExpandEnvironmentStringsA(tmp, PATH_HOSTS, MAX_PATH); RegCloseKey(hkeyHosts); + if (strlen(PATH_HOSTS)+strlen(WIN_PATH_HOSTS) >= MAX_PATH) { + return ARES_ENOTFOUND; + } strcat(PATH_HOSTS, WIN_PATH_HOSTS); #elif defined(WATT32) const char *PATH_HOSTS = _w32_GetHostsFile(); diff --git a/deps/cares/src/lib/ares_init.c b/deps/cares/src/lib/ares_init.c index dc094c4df6eb69..dc3c1e4f537e16 100644 --- a/deps/cares/src/lib/ares_init.c +++ b/deps/cares/src/lib/ares_init.c @@ -102,6 +102,20 @@ static int server_sort_cb(const void *data1, const void *data2) if (s1->consec_failures > s2->consec_failures) { return 1; } + + /* Among servers with the same failure count, prefer the one that failed + * least recently so that a server that went down long ago (and may have + * since recovered) is tried before one that just failed. This also keeps + * retries rotating across servers once the failure counts saturate at + * SERVER_CONSEC_FAILURES_CAP. Healthy servers all have a zeroed retry + * time and fall through to configuration order. */ + if (s1->next_retry_time.sec != s2->next_retry_time.sec) { + return s1->next_retry_time.sec < s2->next_retry_time.sec ? -1 : 1; + } + if (s1->next_retry_time.usec != s2->next_retry_time.usec) { + return s1->next_retry_time.usec < s2->next_retry_time.usec ? -1 : 1; + } + if (s1->idx < s2->idx) { return -1; } diff --git a/deps/cares/src/lib/ares_library_init.c b/deps/cares/src/lib/ares_library_init.c index 2b91692baec6d5..10c88de0095793 100644 --- a/deps/cares/src/lib/ares_library_init.c +++ b/deps/cares/src/lib/ares_library_init.c @@ -105,6 +105,27 @@ void *ares_realloc_zero(void *ptr, size_t orig_size, size_t new_size) return p; } +void *ares_malloc_zero_array(size_t num, size_t size) +{ + size_t total; + if (ares_size_t_mul_overflow(num, size, &total)) { + return NULL; + } + return ares_malloc_zero(total); +} + +void *ares_realloc_zero_array(void *ptr, size_t orig_num, size_t new_num, + size_t size) +{ + size_t orig_total; + size_t new_total; + if (ares_size_t_mul_overflow(orig_num, size, &orig_total) || + ares_size_t_mul_overflow(new_num, size, &new_total)) { + return NULL; + } + return ares_realloc_zero(ptr, orig_total, new_total); +} + int ares_library_init(int flags) { if (ares_initialized) { diff --git a/deps/cares/src/lib/ares_private.h b/deps/cares/src/lib/ares_private.h index d6bd426d39c180..04d4576ed203f1 100644 --- a/deps/cares/src/lib/ares_private.h +++ b/deps/cares/src/lib/ares_private.h @@ -131,6 +131,13 @@ W32_FUNC const char *_w32_GetHostsFile(void); #define DEFAULT_SERVER_RETRY_CHANCE 10 #define DEFAULT_SERVER_RETRY_DELAY 5000 +/* Upper bound on the consecutive failure count tracked per server. Only the + * relative order of the counts is used for server selection, so magnitude + * beyond "clearly down" carries no additional signal. Capping it bounds how + * long a server that failed for an extended period sorts behind other failed + * servers once it comes back online. */ +#define SERVER_CONSEC_FAILURES_CAP 16 + typedef void (*ares_query_enqueue_cb)(void *data); struct ares_query; @@ -418,6 +425,9 @@ ares_status_t ares_parse_sortlist(struct apattern **sortlist, size_t *nsort, ares_status_t ares_lookup_hostaliases(const ares_channel_t *channel, const char *name, char **alias); +ares_bool_t ares_parse_port(const char *str, unsigned short *port, + ares_bool_t allow_zero); + ares_status_t ares_cat_domain(const char *name, const char *domain, char **s); ares_status_t ares_sortaddrinfo(ares_channel_t *channel, struct ares_addrinfo_node *ai_node); @@ -539,10 +549,15 @@ void ares_gethostbyaddr_nolock(ares_channel_t *channel, const void *addr, * ares_free()'d by the caller. * \param[in] is_hostname if ARES_TRUE, will validate the character set for * a valid hostname or will return error. + * \param[in] allow_compression if ARES_FALSE, a compression pointer + * encountered while parsing the name will be rejected + * with ARES_EBADNAME. Used for RR types (e.g. SRV) + * whose RDATA names must not use name compression. * \return ARES_SUCCESS on success */ ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name, - ares_bool_t is_hostname); + ares_bool_t is_hostname, + ares_bool_t allow_compression); /*! Write the DNS name to the buffer in the DNS domain-name syntax as a * series of labels. The maximum domain name length is 255 characters with diff --git a/deps/cares/src/lib/ares_process.c b/deps/cares/src/lib/ares_process.c index 9352968419ef8e..b2316c4b333cab 100644 --- a/deps/cares/src/lib/ares_process.c +++ b/deps/cares/src/lib/ares_process.c @@ -45,7 +45,6 @@ #include -static void timeadd(ares_timeval_t *now, size_t millisecs); static ares_status_t process_write(ares_channel_t *channel, ares_socket_t write_fd); static ares_status_t process_read(ares_channel_t *channel, @@ -66,6 +65,11 @@ static void end_query(ares_channel_t *channel, ares_server_t *server, ares_query_t *query, ares_status_t status, ares_dns_record_t *dnsrec, ares_array_t **requeue); +static ares_status_t ares_send_query_int(ares_server_t *requested_server, + ares_query_t *query, + const ares_timeval_t *now, + ares_array_t **requeue); +static void ares_detach_query(ares_query_t *query); static void ares_query_remove_from_conn(ares_query_t *query) { @@ -124,13 +128,22 @@ static void server_increment_failures(ares_server_t *server, return; /* LCOV_EXCL_LINE: DefensiveCoding */ } - server->consec_failures++; - ares_slist_node_reinsert(node); + /* Cap the failure count. Only the relative order matters for server + * selection, and an uncapped count would require an unbounded number of + * failures on other servers before a server that failed during an + * extended outage could be selected again. */ + if (server->consec_failures < SERVER_CONSEC_FAILURES_CAP) { + server->consec_failures++; + } + /* Must update the retry time before reinserting since the sort uses it to + * order servers with the same failure count */ ares_tvnow(&next_retry_time); - timeadd(&next_retry_time, channel->server_retry_delay); + ares_timeval_add(&next_retry_time, channel->server_retry_delay); server->next_retry_time = next_retry_time; + ares_slist_node_reinsert(node); + invoke_server_state_cb(server, ARES_FALSE, used_tcp == ARES_TRUE ? ARES_SERV_STATE_TCP : ARES_SERV_STATE_UDP); @@ -148,12 +161,13 @@ static void server_set_good(ares_server_t *server, ares_bool_t used_tcp) if (server->consec_failures > 0) { server->consec_failures = 0; + /* Must update the retry time before reinserting since the sort uses it + * to order servers with the same failure count */ + server->next_retry_time.sec = 0; + server->next_retry_time.usec = 0; ares_slist_node_reinsert(node); } - server->next_retry_time.sec = 0; - server->next_retry_time.usec = 0; - invoke_server_state_cb(server, ARES_TRUE, used_tcp == ARES_TRUE ? ARES_SERV_STATE_TCP : ARES_SERV_STATE_UDP); @@ -178,18 +192,6 @@ ares_bool_t ares_timedout(const ares_timeval_t *now, : ARES_FALSE; } -/* add the specific number of milliseconds to the time in the first argument */ -static void timeadd(ares_timeval_t *now, size_t millisecs) -{ - now->sec += (ares_int64_t)millisecs / 1000; - now->usec += (unsigned int)((millisecs % 1000) * 1000); - - if (now->usec >= 1000000) { - now->sec += now->usec / 1000000; - now->usec %= 1000000; - } -} - static ares_status_t ares_process_fds_nolock(ares_channel_t *channel, const ares_fd_events_t *events, size_t nevents, unsigned int flags) @@ -445,12 +447,19 @@ void ares_process_pending_write(ares_channel_t *channel) ares_channel_unlock(channel); } -static ares_status_t read_conn_packets(ares_conn_t *conn) +static ares_status_t read_conn_packets(ares_conn_t *conn, + ares_bool_t *conn_error) { ares_bool_t read_again; ares_conn_err_t err; const ares_channel_t *channel = conn->server->channel; + if (conn_error == NULL) { + return ARES_EFORMERR; + } + + *conn_error = ARES_FALSE; + do { size_t count; size_t len = 65535; @@ -498,6 +507,18 @@ static ares_status_t read_conn_packets(ares_conn_t *conn) /* If UDP, overwrite length */ if (!(conn->flags & ARES_CONN_FLAG_TCP)) { + /* The read buffer is grown in powers of two, so a single recvfrom() can + * return more than the 2-byte length prefix is able to represent. A + * datagram that large can't be a valid DNS message, and writing the + * truncated (unsigned short)count would desync the framing of everything + * buffered after it, so discard it. Standard UDP can't actually deliver + * a payload this large (max is 65507 IPv4 / 65527 IPv6); the only vector + * is IPv6 jumbograms (RFC 2675), which DNS never uses. This is purely + * defense-in-depth. */ + if (count > 65535) { + ares_buf_set_length(conn->in_buf, start_len); + break; + } len = ares_buf_len(conn->in_buf); ares_buf_set_length(conn->in_buf, start_len); ares_buf_append_be16(conn->in_buf, (unsigned short)count); @@ -508,8 +529,15 @@ static ares_status_t read_conn_packets(ares_conn_t *conn) } while (read_again); if (err != ARES_CONN_ERR_SUCCESS && err != ARES_CONN_ERR_WOULDBLOCK) { - handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED); - return ARES_ECONNREFUSED; + /* If there is no packet data buffered, preserve the historical + * immediate connection-failure behavior so retries happen promptly. + * Only defer if there is buffered data to parse first. */ + if (ares_buf_len(conn->in_buf) == 0) { + handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED); + return ARES_ECONNREFUSED; + } + + *conn_error = ARES_TRUE; } return ARES_SUCCESS; @@ -573,6 +601,79 @@ static ares_status_t ares_append_endqueue(ares_array_t **requeue, dnsrec); } +/* Drain the deferred requeue/endqueue list iteratively. All flush sites + * (read_answers(), process_timeouts(), and ares_send_query()) funnel through + * here so that: + * 1. Retries are re-dispatched by appending to this same list and looping, + * rather than recursing ares_requeue_query() -> ares_send_query() until + * the stack is exhausted (issue #1043). + * 2. A query is fully detached from all lookup lists before its callback is + * invoked, so a reentrant ares_cancel() from within that callback cannot + * find and free the same query, which would otherwise double-free it + * (CVE-2026-33630 / GHSA-6wfj-rwm7-3542). + * + * On return the list has been fully processed, an empty-queue notification has + * been sent if appropriate, and *requeue has been destroyed and set to NULL. */ +static ares_status_t ares_flush_requeue(ares_channel_t *channel, + const ares_timeval_t *now, + ares_array_t **requeue) +{ + ares_status_t status = ARES_SUCCESS; + + if (requeue == NULL) { + return status; + } + + while (*requeue != NULL && ares_array_len(*requeue) > 0) { + ares_query_t *query; + ares_requeue_t entry; + ares_status_t internal_status; + + internal_status = ares_array_claim_at(&entry, sizeof(entry), *requeue, 0); + if (internal_status != ARES_SUCCESS) { + break; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + + query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid); + + if (entry.type == REQUEUE_REQUEUE) { + /* Query disappeared (e.g. a prior callback in this drain cancelled it) */ + if (query == NULL) { + continue; + } + /* Re-dispatch via the internal entrypoint so any further requeues are + * appended back onto this same list and drained by the loop above, + * rather than recursing. */ + internal_status = ares_send_query_int(entry.server, query, now, requeue); + /* We only care about ARES_ENOMEM */ + if (internal_status == ARES_ENOMEM) { + status = ARES_ENOMEM; + } + } else { /* REQUEUE_ENDQUERY */ + if (query != NULL) { + /* Detach the query from all lookup lists BEFORE invoking the callback. + * Otherwise a reentrant ares_cancel() from within the callback would + * find this query still linked in all_queries/queries_by_qid, free it, + * and the ares_free_query() below would then double-free it. */ + ares_detach_query(query); + query->callback(query->arg, entry.status, query->timeouts, + entry.dnsrec); + ares_free_query(query); + } + ares_dns_record_destroy(entry.dnsrec); + } + } + + /* Don't forget to send notification if queue emptied */ + if (*requeue != NULL) { + ares_queue_notify_empty(channel); + } + ares_array_destroy(*requeue); + *requeue = NULL; + + return status; +} + static ares_status_t read_answers(ares_conn_t *conn, const ares_timeval_t *now) { ares_status_t status; @@ -625,43 +726,11 @@ static ares_status_t read_answers(ares_conn_t *conn, const ares_timeval_t *now) } cleanup: - - /* Flush requeue */ - while (ares_array_len(requeue) > 0) { - ares_query_t *query; - ares_requeue_t entry; - ares_status_t internal_status; - - internal_status = ares_array_claim_at(&entry, sizeof(entry), requeue, 0); - if (internal_status != ARES_SUCCESS) { - break; - } - - query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid); - - if (entry.type == REQUEUE_REQUEUE) { - /* query disappeared */ - if (query == NULL) { - continue; - } - internal_status = ares_send_query(entry.server, query, now); - /* We only care about ARES_ENOMEM */ - if (internal_status == ARES_ENOMEM) { - status = ARES_ENOMEM; - } - } else { /* REQUEUE_ENDQUERY */ - if (query != NULL) { - query->callback(query->arg, entry.status, query->timeouts, entry.dnsrec); - ares_free_query(query); - } - ares_dns_record_destroy(entry.dnsrec); - } - } - /* Don't forget to send notification if queue emptied */ - if (requeue != NULL) { - ares_queue_notify_empty(channel); + /* Flush requeue - re-dispatch retries and invoke deferred callbacks + * iteratively and safely */ + if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { + status = ARES_ENOMEM; } - ares_array_destroy(requeue); return status; } @@ -671,24 +740,32 @@ static ares_status_t process_read(ares_channel_t *channel, const ares_timeval_t *now) { ares_conn_t *conn = ares_conn_from_fd(channel, read_fd); + ares_bool_t conn_error; ares_status_t status; if (conn == NULL) { return ARES_SUCCESS; } - /* TODO: There might be a potential issue here where there was a read that - * read some data, then looped and read again and got a disconnect. - * Right now, that would cause a resend instead of processing the data - * we have. This is fairly unlikely to occur due to only looping if - * a full buffer of 65535 bytes was read. */ - status = read_conn_packets(conn); + status = read_conn_packets(conn, &conn_error); if (status != ARES_SUCCESS) { return status; } - return read_answers(conn, now); + status = read_answers(conn, now); + if (status != ARES_SUCCESS) { + return status; + } + + if (conn_error) { + conn = ares_conn_from_fd(channel, read_fd); + if (conn != NULL) { + handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED); + } + } + + return ARES_SUCCESS; } /* If any queries have timed out, note the timeout and move them on. */ @@ -696,7 +773,8 @@ static ares_status_t process_timeouts(ares_channel_t *channel, const ares_timeval_t *now) { ares_slist_node_t *node; - ares_status_t status = ARES_SUCCESS; + ares_status_t status = ARES_SUCCESS; + ares_array_t *requeue = NULL; /* Just keep popping off the first as this list will re-sort as things come * and go. We don't want to try to rely on 'next' as some operation might @@ -714,14 +792,26 @@ static ares_status_t process_timeouts(ares_channel_t *channel, query->timeouts++; conn = query->conn; + /* Retire this connection for NEW queries. A timeout suggests packets are + * being dropped on it, so route new queries to a fresh source port while + * the in-flight queries here drain (it is cleaned up once idle). This is + * per-connection so a transient failure doesn't stop reuse of healthy + * connections to the same server. */ + conn->flags |= ARES_CONN_FLAG_NONEW; server_increment_failures(conn->server, query->using_tcp); - status = ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, - NULL); + status = + ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, &requeue); if (status == ARES_ENOMEM) { goto done; } } done: + /* Flush requeue - re-dispatch retries and invoke deferred callbacks + * iteratively and safely */ + if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { + status = ARES_ENOMEM; + } + if (status == ARES_ENOMEM) { return ARES_ENOMEM; } @@ -871,7 +961,7 @@ static ares_status_t process_answer(ares_channel_t *channel, if (issue_might_be_edns(query->query, rdnsrec)) { status = rewrite_without_edns(query); if (status != ARES_SUCCESS) { - end_query(channel, server, query, status, NULL, NULL); + end_query(channel, server, query, status, NULL, requeue); goto cleanup; } @@ -975,6 +1065,11 @@ ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, { ares_channel_t *channel = query->channel; size_t max_tries = ares_slist_len(channel->servers) * channel->tries; + ares_server_t *server = NULL; + + if (query->conn != NULL) { + server = query->conn->server; + } ares_query_remove_from_conn(query); @@ -999,7 +1094,7 @@ ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, query->error_status = ARES_ETIMEOUT; } - end_query(channel, NULL, query, query->error_status, dnsrec, requeue); + end_query(channel, server, query, query->error_status, dnsrec, requeue); return ARES_ETIMEOUT; } @@ -1102,22 +1197,23 @@ static void ares_probe_failed_server(ares_channel_t *channel, } /* Select the first server with failures to retry that has passed the retry - * timeout and doesn't already have a pending probe */ + * timeout and doesn't already have a pending probe. Skip the server the + * triggering query was just sent to since that query is already exercising + * it. */ ares_tvnow(&now); for (node = ares_slist_node_first(channel->servers); node != NULL; node = ares_slist_node_next(node)) { ares_server_t *node_val = ares_slist_node_val(node); - if (node_val != NULL && node_val->consec_failures > 0 && - !node_val->probe_pending && + if (node_val != NULL && node_val != server && + node_val->consec_failures > 0 && !node_val->probe_pending && ares_timedout(&now, &node_val->next_retry_time)) { probe_server = node_val; break; } } - /* Either nothing to probe or the query was enqueud to the same server - * we were going to probe. Do nothing. */ - if (probe_server == NULL || server == probe_server) { + /* Nothing to probe */ + if (probe_server == NULL) { return; } @@ -1148,7 +1244,12 @@ static size_t ares_calc_query_timeout(const ares_query_t *query, * retry from the last retry */ rounds = (query->try_count / num_servers); if (rounds > 0) { - timeplus <<= rounds; + if (rounds >= sizeof(timeplus) * CHAR_BIT || + timeplus > (SIZE_MAX >> rounds)) { + timeplus = SIZE_MAX; + } else { + timeplus <<= rounds; + } } if (channel->maxtimeout && timeplus > channel->maxtimeout) { @@ -1205,9 +1306,13 @@ static ares_conn_t *ares_fetch_connection(const ares_channel_t *channel, return NULL; } - /* If the associated server has failures, don't use it. It should be cleaned - * up later. */ - if (conn->server->consec_failures > 0) { + /* Don't hand new queries to a connection that has been retired (e.g. it saw + * a timeout). It keeps servicing its in-flight queries and is cleaned up + * once idle. Note this is a per-connection check: a server-wide failure + * counter must not be used here or a single transient failure would evict + * every (including healthy) connection to the server and spawn a new socket + * per query. */ + if (conn->flags & ARES_CONN_FLAG_NONEW) { return NULL; } @@ -1262,8 +1367,44 @@ static ares_status_t ares_conn_query_write(ares_conn_t *conn, return ares_conn_flush(conn); } +/* Public entrypoint. Establishes a requeue list and drives + * ares_send_query_int() plus any retries/deferred callbacks it produces + * iteratively, so a chain of retryable failures can never recurse until the + * stack is exhausted (#1043). */ ares_status_t ares_send_query(ares_server_t *requested_server, ares_query_t *query, const ares_timeval_t *now) +{ + ares_channel_t *channel = query->channel; + ares_array_t *requeue = NULL; + unsigned short qid = query->qid; + ares_status_t status; + + status = ares_send_query_int(requested_server, query, now, &requeue); + + /* Drain any retries/deferred callbacks this send produced. + * ares_flush_requeue() always fully processes and destroys the list (even on + * ENOMEM), and sends the empty-queue notification if needed. */ + if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { + status = ARES_ENOMEM; + } + + /* A retry may have been deferred (returning ARES_SUCCESS from the append) + * and then terminally failed while draining, in which case the query has + * been freed. Do not dereference 'query' here. If it is no longer tracked + * it ended, so don't report success to the caller (which would, e.g., cause + * ares_send_nolock() to write to a now-freed *qid). */ + if (status == ARES_SUCCESS && + ares_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) { + status = ARES_ETIMEOUT; + } + + return status; +} + +static ares_status_t ares_send_query_int(ares_server_t *requested_server, + ares_query_t *query, + const ares_timeval_t *now, + ares_array_t **requeue) { ares_channel_t *channel = query->channel; ares_server_t *server; @@ -1286,14 +1427,16 @@ ares_status_t ares_send_query(ares_server_t *requested_server, } if (server == NULL) { - end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, NULL); + end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, requeue); return ARES_ENOSERVER; } - /* If a query is directed to a specific query, or the server chosen has - * failures, or the query is being retried, don't probe for downed servers */ - if (requested_server != NULL || server->consec_failures > 0 || - query->try_count != 0) { + /* If a query is directed to a specific server, or the query is being + * retried, don't probe for downed servers. Note that the chosen server + * having failures itself does NOT disable probing: when every server has + * failures (e.g. during an extended outage), probes are the only way a + * recovered server that sorts behind the current best can be noticed. */ + if (requested_server != NULL || query->try_count != 0) { probe_downed_server = ARES_FALSE; } @@ -1310,11 +1453,11 @@ ares_status_t ares_send_query(ares_server_t *requested_server, case ARES_ECONNREFUSED: case ARES_EBADFAMILY: server_increment_failures(server, query->using_tcp); - return ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); + return ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); /* Anything else is not retryable, likely ENOMEM */ default: - end_query(channel, server, query, status, NULL, NULL); + end_query(channel, server, query, status, NULL, requeue); return status; } } @@ -1328,7 +1471,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, case ARES_ENOMEM: /* Not retryable */ - end_query(channel, server, query, status, NULL, NULL); + end_query(channel, server, query, status, NULL, requeue); return status; /* These conditions are retryable as they are server-specific @@ -1336,7 +1479,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, case ARES_ECONNREFUSED: case ARES_EBADFAMILY: handle_conn_error(conn, ARES_TRUE, status); - status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); + status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); if (status == ARES_ETIMEOUT) { status = ARES_ECONNREFUSED; } @@ -1344,7 +1487,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, default: server_increment_failures(server, query->using_tcp); - status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); + status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); return status; } @@ -1355,12 +1498,12 @@ ares_status_t ares_send_query(ares_server_t *requested_server, ares_slist_node_destroy(query->node_queries_by_timeout); query->ts = *now; query->timeout = *now; - timeadd(&query->timeout, timeplus); + ares_timeval_add(&query->timeout, timeplus); query->node_queries_by_timeout = ares_slist_insert(channel->queries_by_timeout, query); if (!query->node_queries_by_timeout) { /* LCOV_EXCL_START: OutOfMemory */ - end_query(channel, server, query, ARES_ENOMEM, NULL, NULL); + end_query(channel, server, query, ARES_ENOMEM, NULL, requeue); return ARES_ENOMEM; /* LCOV_EXCL_STOP */ } @@ -1373,7 +1516,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, if (query->node_queries_to_conn == NULL) { /* LCOV_EXCL_START: OutOfMemory */ - end_query(channel, server, query, ARES_ENOMEM, NULL, NULL); + end_query(channel, server, query, ARES_ENOMEM, NULL, requeue); return ARES_ENOMEM; /* LCOV_EXCL_STOP */ } diff --git a/deps/cares/src/lib/ares_qcache.c b/deps/cares/src/lib/ares_qcache.c index 4050ff54bf1667..ad3bc83505af07 100644 --- a/deps/cares/src/lib/ares_qcache.c +++ b/deps/cares/src/lib/ares_qcache.c @@ -371,9 +371,11 @@ static ares_status_t ares_qcache_insert_int(ares_qcache_t *qcache, /* LCOV_EXCL_START: OutOfMemory */ fail: - if (entry != NULL && entry->key != NULL) { - ares_htable_strvp_remove(qcache->cache, entry->key); - ares_free(entry->key); + if (entry != NULL) { + if (entry->key != NULL) { + ares_htable_strvp_remove(qcache->cache, entry->key); + ares_free(entry->key); + } ares_free(entry); } return ARES_ENOMEM; diff --git a/deps/cares/src/lib/ares_socket.c b/deps/cares/src/lib/ares_socket.c index 516852a84abfb8..4ad3034021a0f7 100644 --- a/deps/cares/src/lib/ares_socket.c +++ b/deps/cares/src/lib/ares_socket.c @@ -188,6 +188,8 @@ ares_conn_err_t ares_socket_recvfrom(ares_channel_t *channel, ares_socket_t s, { ares_ssize_t rv; + *read_bytes = 0; + rv = channel->sock_funcs.arecvfrom(s, data, data_len, flags, from, from_len, channel->sock_func_cb_data); diff --git a/deps/cares/src/lib/ares_sysconfig.c b/deps/cares/src/lib/ares_sysconfig.c index 286db60328f45b..9e389c17672a1f 100644 --- a/deps/cares/src/lib/ares_sysconfig.c +++ b/deps/cares/src/lib/ares_sysconfig.c @@ -218,13 +218,16 @@ static ares_status_t ares_init_sysconfig_android(const ares_channel_t *channel, status = ares_sconfig_append_fromstr(channel, &sysconfig->sconfig, dns_servers[i], ARES_TRUE); if (status != ARES_SUCCESS) { - return status; + break; } } for (i = 0; i < num_servers; i++) { ares_free(dns_servers[i]); } ares_free(dns_servers); + if (status != ARES_SUCCESS) { + return status; + } } domains = ares_get_android_search_domains_list(); @@ -273,7 +276,7 @@ static ares_status_t * 3. if confstr(_CS_DOMAIN, ...) this is the domain name. Use this as * preference over anything else found. */ - ares_buf_t *buf = ares_buf_create(); + ares_buf_t *buf = NULL; unsigned char *data = NULL; size_t data_size = 0; ares_bool_t process_resolvconf = ARES_TRUE; @@ -330,7 +333,7 @@ static ares_status_t char domain[256]; size_t domain_len; - domain_len = confstr(_CS_DOMAIN, domain, sizeof(domain_len)); + domain_len = confstr(_CS_DOMAIN, domain, sizeof(domain)); if (domain_len != 0) { ares_strsplit_free(sysconfig->domains, sysconfig->ndomains); sysconfig->domains = ares_strsplit(domain, ", ", &sysconfig->ndomains); diff --git a/deps/cares/src/lib/ares_sysconfig_files.c b/deps/cares/src/lib/ares_sysconfig_files.c index a6c2a8e62bb34f..2caa3b200075b8 100644 --- a/deps/cares/src/lib/ares_sysconfig_files.c +++ b/deps/cares/src/lib/ares_sysconfig_files.c @@ -27,6 +27,8 @@ #include "ares_private.h" +#include + #ifdef HAVE_SYS_PARAM_H # include #endif @@ -170,8 +172,8 @@ static ares_status_t parse_sort(ares_buf_t *buf, struct apattern *pat) if (ares_str_isnum(maskstr)) { /* Numeric mask */ - int mask = atoi(maskstr); - if (mask < 0 || mask > 128) { + unsigned int mask; + if (!ares_str_parse_uint(maskstr, 128, &mask)) { return ARES_EBADSTR; } if (pat->addr.family == AF_INET && mask > 32) { @@ -340,12 +342,17 @@ static ares_status_t config_lookup(ares_sysconfig_t *sysconfig, ares_buf_t *buf, const char *value = lookups[i]; char ch; + /* AIX /etc/netsvc.conf uses address-family-suffixed tokens such as + * "bind4"/"bind6" and "local4"/"local6" in addition to the bare forms. */ if (ares_strcaseeq(value, "dns") || ares_strcaseeq(value, "bind") || + ares_strcaseeq(value, "bind4") || ares_strcaseeq(value, "bind6") || ares_strcaseeq(value, "resolv") || ares_strcaseeq(value, "resolve")) { ch = 'b'; } else if (ares_strcaseeq(value, "files") || ares_strcaseeq(value, "file") || - ares_strcaseeq(value, "local")) { + ares_strcaseeq(value, "local") || + ares_strcaseeq(value, "local4") || + ares_strcaseeq(value, "local6")) { ch = 'f'; } else { continue; @@ -401,20 +408,25 @@ static ares_status_t process_option(ares_sysconfig_t *sysconfig, key = kv[0]; if (num == 2) { - val = kv[1]; - valint = (unsigned int)strtoul(val, NULL, 10); + val = kv[1]; + if (!ares_str_parse_uint(val, UINT_MAX, &valint)) { + status = ARES_EBADSTR; + goto done; + } } if (ares_streq(key, "ndots")) { sysconfig->ndots = valint; } else if (ares_streq(key, "retrans") || ares_streq(key, "timeout")) { if (valint == 0) { - return ARES_EFORMERR; + status = ARES_EFORMERR; + goto done; } sysconfig->timeout_ms = valint * 1000; } else if (ares_streq(key, "retry") || ares_streq(key, "attempts")) { if (valint == 0) { - return ARES_EFORMERR; + status = ARES_EFORMERR; + goto done; } sysconfig->tries = valint; } else if (ares_streq(key, "rotate")) { diff --git a/deps/cares/src/lib/ares_sysconfig_win.c b/deps/cares/src/lib/ares_sysconfig_win.c index 94a3817de348bb..e0daf1103e2cec 100644 --- a/deps/cares/src/lib/ares_sysconfig_win.c +++ b/deps/cares/src/lib/ares_sysconfig_win.c @@ -114,56 +114,69 @@ static ares_bool_t get_REG_SZ(HKEY hKey, const WCHAR *leafKeyName, char **outptr /* Convert to UTF8 */ len = WideCharToMultiByte(CP_UTF8, 0, val, -1, NULL, 0, NULL, NULL); if (len == 0) { + ares_free(val); return ARES_FALSE; } *outptr = ares_malloc_zero((size_t)len + 1); + if (*outptr == NULL) { + ares_free(val); + return ARES_FALSE; + } if (WideCharToMultiByte(CP_UTF8, 0, val, -1, *outptr, len, NULL, NULL) == 0) { ares_free(*outptr); *outptr = NULL; + ares_free(val); return ARES_FALSE; } + ares_free(val); return ARES_TRUE; } -static void commanjoin(char **dst, const char * const src, const size_t len) +static ares_status_t sysconfig_commajoin(ares_buf_t *buf, const char *src, + ares_bool_t asciionly) { - char *newbuf; - size_t newsize; + ares_status_t status; - /* 1 for terminating 0 and 2 for , and terminating 0 */ - newsize = len + (*dst ? (ares_strlen(*dst) + 2) : 1); - newbuf = ares_realloc(*dst, newsize); - if (!newbuf) { - return; + if (buf == NULL) { + return ARES_ENOMEM; } - if (*dst == NULL) { - *newbuf = '\0'; + + if (src == NULL || *src == '\0') { + return ARES_SUCCESS; } - *dst = newbuf; - if (ares_strlen(*dst) != 0) { - strcat(*dst, ","); + + if (asciionly && !ares_str_isprint(src, ares_strlen(src))) { + return ARES_SUCCESS; } - strncat(*dst, src, len); -} -/* - * commajoin() - * - * RTF code. - */ -static void commajoin(char **dst, const char *src) -{ - commanjoin(dst, src, ares_strlen(src)); + if (ares_buf_len(buf) > 0) { + status = ares_buf_append_byte(buf, ','); + if (status != ARES_SUCCESS) { + return status; + } + } + + return ares_buf_append_str(buf, src); } -static void commajoin_asciionly(char **dst, const char *src) +/* Read a REG_SZ value and comma-append it (ASCII-only) to *buf. + * On allocation failure, destroys *buf and sets it to NULL. */ +static void reg_commajoin(ares_buf_t **buf, HKEY key, const WCHAR *leaf) { - if (!ares_str_isprint(src, ares_strlen(src))) { + char *p = NULL; + + if (*buf == NULL || !get_REG_SZ(key, leaf, &p)) { return; } - commanjoin(dst, src, ares_strlen(src)); + + if (sysconfig_commajoin(*buf, p, ARES_TRUE) != ARES_SUCCESS) { + ares_buf_destroy(*buf); + *buf = NULL; + } + + ares_free(p); } /* A structure to hold the string form of IPv4 and IPv6 addresses so we can @@ -494,8 +507,10 @@ static ares_bool_t get_DNS_Windows(char **outptr) /* Join them all into a single string, removing duplicates. */ { - size_t i; - for (i = 0; i < addressesIndex; ++i) { + size_t i; + ares_buf_t *buf = ares_buf_create(); + + for (i = 0; buf != NULL && i < addressesIndex; ++i) { size_t j; /* Look for this address text appearing previously in the results. */ for (j = 0; j < i; ++j) { @@ -505,10 +520,22 @@ static ares_bool_t get_DNS_Windows(char **outptr) } /* Iff we didn't emit this address already, emit it now. */ if (j == i) { - /* Add that to outptr (if we can). */ - commajoin(outptr, addresses[i].text); + /* Add that to buf (if we can). */ + if (sysconfig_commajoin(buf, addresses[i].text, ARES_FALSE) != + ARES_SUCCESS) { + ares_buf_destroy(buf); + buf = NULL; + break; + } } } + + if (buf != NULL && ares_buf_len(buf) == 0) { + ares_buf_destroy(buf); + buf = NULL; + } + + *outptr = ares_buf_finish_str(buf, NULL); } done: @@ -540,51 +567,46 @@ static ares_bool_t get_DNS_Windows(char **outptr) */ static ares_bool_t get_SuffixList_Windows(char **outptr) { - HKEY hKey; - HKEY hKeyEnum; - char keyName[256]; - DWORD keyNameBuffSize; - DWORD keyIdx = 0; - char *p = NULL; + HKEY hKey; + HKEY hKeyEnum; + char keyName[256]; + DWORD keyNameBuffSize; + DWORD keyIdx = 0; + ares_buf_t *buf = ares_buf_create(); *outptr = NULL; + if (buf == NULL) { + return ARES_FALSE; + } + /* 1. Global DNS Suffix Search List */ if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NS_NT_KEY, 0, KEY_READ, &hKey) == ERROR_SUCCESS) { - get_REG_SZ(hKey, SEARCHLIST_KEY, outptr); - if (get_REG_SZ(hKey, DOMAIN_KEY, &p)) { - commajoin_asciionly(outptr, p); - ares_free(p); - p = NULL; - } + reg_commajoin(&buf, hKey, SEARCHLIST_KEY); + reg_commajoin(&buf, hKey, DOMAIN_KEY); RegCloseKey(hKey); } - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NT_DNSCLIENT, 0, KEY_READ, &hKey) == - ERROR_SUCCESS) { - if (get_REG_SZ(hKey, SEARCHLIST_KEY, &p)) { - commajoin_asciionly(outptr, p); - ares_free(p); - p = NULL; - } + if (buf != NULL && + RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NT_DNSCLIENT, 0, KEY_READ, &hKey) == + ERROR_SUCCESS) { + reg_commajoin(&buf, hKey, SEARCHLIST_KEY); RegCloseKey(hKey); } /* 2. Connection Specific Search List composed of: * a. Primary DNS Suffix */ - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_DNSCLIENT, 0, KEY_READ, &hKey) == - ERROR_SUCCESS) { - if (get_REG_SZ(hKey, PRIMARYDNSSUFFIX_KEY, &p)) { - commajoin_asciionly(outptr, p); - ares_free(p); - p = NULL; - } + if (buf != NULL && + RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_DNSCLIENT, 0, KEY_READ, &hKey) == + ERROR_SUCCESS) { + reg_commajoin(&buf, hKey, PRIMARYDNSSUFFIX_KEY); RegCloseKey(hKey); } /* b. Interface SearchList, Domain, DhcpDomain */ - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NS_NT_KEY "\\" INTERFACES_KEY, 0, + if (buf != NULL && + RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NS_NT_KEY "\\" INTERFACES_KEY, 0, KEY_READ, &hKey) == ERROR_SUCCESS) { for (;;) { keyNameBuffSize = sizeof(keyName); @@ -596,27 +618,26 @@ static ares_bool_t get_SuffixList_Windows(char **outptr) ERROR_SUCCESS) { continue; } - /* p can be comma separated (SearchList) */ - if (get_REG_SZ(hKeyEnum, SEARCHLIST_KEY, &p)) { - commajoin_asciionly(outptr, p); - ares_free(p); - p = NULL; - } - if (get_REG_SZ(hKeyEnum, DOMAIN_KEY, &p)) { - commajoin_asciionly(outptr, p); - ares_free(p); - p = NULL; - } - if (get_REG_SZ(hKeyEnum, DHCPDOMAIN_KEY, &p)) { - commajoin_asciionly(outptr, p); - ares_free(p); - p = NULL; - } + /* SearchList can be comma separated */ + reg_commajoin(&buf, hKeyEnum, SEARCHLIST_KEY); + reg_commajoin(&buf, hKeyEnum, DOMAIN_KEY); + reg_commajoin(&buf, hKeyEnum, DHCPDOMAIN_KEY); RegCloseKey(hKeyEnum); + + if (buf == NULL) { + break; + } } RegCloseKey(hKey); } + if (ares_buf_len(buf) == 0) { + ares_buf_destroy(buf); + buf = NULL; + } + + *outptr = ares_buf_finish_str(buf, NULL); + return *outptr != NULL ? ARES_TRUE : ARES_FALSE; } diff --git a/deps/cares/src/lib/ares_timeout.c b/deps/cares/src/lib/ares_timeout.c index 0d2fdcff21f657..dbd5cf443d68f2 100644 --- a/deps/cares/src/lib/ares_timeout.c +++ b/deps/cares/src/lib/ares_timeout.c @@ -32,6 +32,17 @@ #endif +void ares_timeval_add(ares_timeval_t *now, size_t millisecs) +{ + now->sec += (ares_int64_t)millisecs / 1000; + now->usec += (unsigned int)((millisecs % 1000) * 1000); + + if (now->usec >= 1000000) { + now->sec += now->usec / 1000000; + now->usec %= 1000000; + } +} + void ares_timeval_remaining(ares_timeval_t *remaining, const ares_timeval_t *now, const ares_timeval_t *tout) diff --git a/deps/cares/src/lib/ares_update_servers.c b/deps/cares/src/lib/ares_update_servers.c index 70a9381499f8c2..f8d441accf23e1 100644 --- a/deps/cares/src/lib/ares_update_servers.c +++ b/deps/cares/src/lib/ares_update_servers.c @@ -27,6 +27,8 @@ */ #include "ares_private.h" +#include + #ifdef HAVE_ARPA_INET_H # include #endif @@ -234,7 +236,10 @@ static ares_status_t parse_nameserver_uri(ares_buf_t *buf, sconfig->tcp_port = sconfig->udp_port; port = ares_uri_get_query_key(uri, "tcpport"); if (port != NULL) { - sconfig->tcp_port = (unsigned short)atoi(port); + if (!ares_parse_port(port, &sconfig->tcp_port, ARES_TRUE)) { + status = ARES_EBADSTR; + goto done; + } } done: @@ -352,7 +357,9 @@ static ares_status_t parse_nameserver(ares_buf_t *buf, ares_sconfig_t *sconfig) return status; } - sconfig->udp_port = (unsigned short)atoi(portstr); + if (!ares_parse_port(portstr, &sconfig->udp_port, ARES_TRUE)) { + return ARES_EBADSTR; + } sconfig->tcp_port = sconfig->udp_port; } @@ -398,7 +405,13 @@ static ares_status_t ares_sconfig_linklocal(const ares_channel_t *channel, if (ares_str_isnum(ll_iface)) { char ifname[IF_NAMESIZE] = ""; - ll_scope = (unsigned int)atoi(ll_iface); + + /* The interface identifier is all digits but may not fit in an + * unsigned int; parse with the range-checked helper rather than atoi(), + * whose behavior on overflow is undefined. */ + if (!ares_str_parse_uint(ll_iface, UINT_MAX, &ll_scope)) { + return ARES_ENOTFOUND; + } if (channel->sock_funcs.aif_indextoname == NULL || channel->sock_funcs.aif_indextoname(ll_scope, ifname, sizeof(ifname), channel->sock_func_cb_data) == @@ -967,7 +980,8 @@ ares_status_t ares_in_addr_to_sconfig_llist(const struct in_addr *servers, sizeof(sconfig->addr.addr.addr4)); if (ares_llist_insert_last(s, sconfig) == NULL) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + ares_free(sconfig); /* LCOV_EXCL_LINE: OutOfMemory */ + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } } diff --git a/deps/cares/src/lib/dsa/ares_array.c b/deps/cares/src/lib/dsa/ares_array.c index c421d5c5f670bd..e6bce6e41db91e 100644 --- a/deps/cares/src/lib/dsa/ares_array.c +++ b/deps/cares/src/lib/dsa/ares_array.c @@ -171,14 +171,19 @@ void *ares_array_finish(ares_array_t *arr, size_t *num_members) ares_status_t ares_array_set_size(ares_array_t *arr, size_t size) { - void *temp; + void *temp; + size_t rounded_size; if (arr == NULL || size == 0 || size < arr->cnt) { return ARES_EFORMERR; } /* Always operate on powers of 2 */ - size = ares_round_up_pow2(size); + rounded_size = ares_round_up_pow2(size); + if (rounded_size < size) { + return ARES_ENOMEM; /* rounding wrapped around */ + } + size = rounded_size; if (size < ARES__ARRAY_MIN) { size = ARES__ARRAY_MIN; @@ -189,8 +194,8 @@ ares_status_t ares_array_set_size(ares_array_t *arr, size_t size) return ARES_SUCCESS; } - temp = ares_realloc_zero(arr->arr, arr->alloc_cnt * arr->member_size, - size * arr->member_size); + temp = + ares_realloc_zero_array(arr->arr, arr->alloc_cnt, size, arr->member_size); if (temp == NULL) { return ARES_ENOMEM; } @@ -295,7 +300,7 @@ ares_status_t ares_array_insertdata_first(ares_array_t *arr, ares_status_t status; void *ptr = NULL; - status = ares_array_insert_last(&ptr, arr); + status = ares_array_insert_first(&ptr, arr); if (status != ARES_SUCCESS) { return status; } @@ -362,6 +367,16 @@ ares_status_t ares_array_claim_at(void *dest, size_t dest_size, } arr->cnt--; + + /* When empty, reset offset so a later insert doesn't compact from an + * out-of-range index (offset can reach alloc_cnt after front claims). + * This also protects ares_array_finish(), which has the same + * move(0, offset) and would otherwise fail on an offset == alloc_cnt + * array. */ + if (arr->cnt == 0) { + arr->offset = 0; + } + return ARES_SUCCESS; } diff --git a/deps/cares/src/lib/dsa/ares_htable.c b/deps/cares/src/lib/dsa/ares_htable.c index f76b67cae9a668..d6ac7a065a6b02 100644 --- a/deps/cares/src/lib/dsa/ares_htable.c +++ b/deps/cares/src/lib/dsa/ares_htable.c @@ -150,6 +150,10 @@ const void **ares_htable_all_buckets(const ares_htable_t *htable, size_t *num) *num = 0; + if (htable->num_keys == 0) { + return NULL; + } + out = ares_malloc_zero(sizeof(*out) * htable->num_keys); if (out == NULL) { return NULL; /* LCOV_EXCL_LINE */ @@ -172,7 +176,7 @@ const void **ares_htable_all_buckets(const ares_htable_t *htable, size_t *num) * We are doing "hash & (size - 1)" since we are guaranteeing a power of * 2 for size. This is equivalent to "hash % size", but should be more * efficient */ -#define HASH_IDX(h, key) h->hash(key, h->seed) & (h->size - 1) +#define HASH_IDX(h, key) ((h)->hash((key), (h)->seed) & ((h)->size - 1)) static ares_llist_node_t *ares_htable_find(const ares_htable_t *htable, unsigned int idx, const void *key) diff --git a/deps/cares/src/lib/dsa/ares_htable_dict.c b/deps/cares/src/lib/dsa/ares_htable_dict.c index 93d7a20137c8db..cda7a49c79d05d 100644 --- a/deps/cares/src/lib/dsa/ares_htable_dict.c +++ b/deps/cares/src/lib/dsa/ares_htable_dict.c @@ -132,6 +132,7 @@ ares_bool_t ares_htable_dict_insert(ares_htable_dict_t *htable, const char *key, fail: if (bucket) { + ares_free(bucket->key); ares_free(bucket->val); ares_free(bucket); } @@ -223,6 +224,7 @@ char **ares_htable_dict_keys(const ares_htable_dict_t *htable, size_t *num) fail: *num = 0; + ares_free(buckets); ares_free_array(out, cnt, ares_free); return NULL; } diff --git a/deps/cares/src/lib/dsa/ares_llist.c b/deps/cares/src/lib/dsa/ares_llist.c index 6bd7de269a43fb..ce1a31149401f1 100644 --- a/deps/cares/src/lib/dsa/ares_llist.c +++ b/deps/cares/src/lib/dsa/ares_llist.c @@ -103,7 +103,10 @@ static void ares_llist_attach_at(ares_llist_t *list, case ARES__LLIST_INSERT_BEFORE: node->next = at; node->prev = at->prev; - at->prev = node; + if (at->prev) { + at->prev->next = node; + } + at->prev = node; break; } if (list->tail == NULL) { diff --git a/deps/cares/src/lib/event/ares_event_configchg.c b/deps/cares/src/lib/event/ares_event_configchg.c index add729574e46c3..d73c4862058c97 100644 --- a/deps/cares/src/lib/event/ares_event_configchg.c +++ b/deps/cares/src/lib/event/ares_event_configchg.c @@ -207,12 +207,14 @@ void ares_event_configchg_destroy(ares_event_configchg_t *configchg) # ifdef HAVE_REGISTERWAITFORSINGLEOBJECT if (configchg->regip4_wait != NULL) { - UnregisterWait(configchg->regip4_wait); + /* Use INVALID_HANDLE_VALUE to wait for any running callback to complete + * before returning, preventing use-after-free of configchg */ + UnregisterWaitEx(configchg->regip4_wait, INVALID_HANDLE_VALUE); configchg->regip4_wait = NULL; } if (configchg->regip6_wait != NULL) { - UnregisterWait(configchg->regip6_wait); + UnregisterWaitEx(configchg->regip6_wait, INVALID_HANDLE_VALUE); configchg->regip6_wait = NULL; } diff --git a/deps/cares/src/lib/event/ares_event_wake_pipe.c b/deps/cares/src/lib/event/ares_event_wake_pipe.c index cd1534bbbd586c..2ea72c29415230 100644 --- a/deps/cares/src/lib/event/ares_event_wake_pipe.c +++ b/deps/cares/src/lib/event/ares_event_wake_pipe.c @@ -117,7 +117,13 @@ static void ares_pipeevent_signal(const ares_event_t *e) } p = e->data; - (void)write(p->filedes[1], "1", 1); + /* Best-effort wakeup. A failed or short write is harmless: it can only + * happen when the pipe buffer is already full, which means a wakeup is + * already pending. The result is consumed to satisfy warn_unused_result + * (a plain (void) cast does not silence it under _FORTIFY_SOURCE). */ + if (write(p->filedes[1], "1", 1) < 0) { + return; + } } static void ares_pipeevent_cb(ares_event_thread_t *e, ares_socket_t fd, diff --git a/deps/cares/src/lib/event/ares_event_win32.c b/deps/cares/src/lib/event/ares_event_win32.c index d7d1d65735082d..d558b6930a66f2 100644 --- a/deps/cares/src/lib/event/ares_event_win32.c +++ b/deps/cares/src/lib/event/ares_event_win32.c @@ -707,6 +707,9 @@ static ares_bool_t ares_evsys_win32_event_add(ares_event_t *event) ares_bool_t rc = ARES_FALSE; ed = ares_malloc_zero(sizeof(*ed)); + if (ed == NULL) { + return ARES_FALSE; /* LCOV_EXCL_LINE: OutOfMemory */ + } ed->event = event; ed->socket = event->fd; ed->base_socket = ARES_SOCKET_BAD; diff --git a/deps/cares/src/lib/include/ares_mem.h b/deps/cares/src/lib/include/ares_mem.h index 371cd4266dc720..d980923e5817a2 100644 --- a/deps/cares/src/lib/include/ares_mem.h +++ b/deps/cares/src/lib/include/ares_mem.h @@ -34,5 +34,8 @@ CARES_EXTERN void ares_free(void *ptr); CARES_EXTERN void *ares_malloc_zero(size_t size); CARES_EXTERN void *ares_realloc_zero(void *ptr, size_t orig_size, size_t new_size); +CARES_EXTERN void *ares_malloc_zero_array(size_t num, size_t size); +CARES_EXTERN void *ares_realloc_zero_array(void *ptr, size_t orig_num, + size_t new_num, size_t size); #endif diff --git a/deps/cares/src/lib/include/ares_str.h b/deps/cares/src/lib/include/ares_str.h index 4ee339510bf026..c7fdf4fc4c8059 100644 --- a/deps/cares/src/lib/include/ares_str.h +++ b/deps/cares/src/lib/include/ares_str.h @@ -60,6 +60,23 @@ CARES_EXTERN size_t ares_strcpy(char *dest, const char *src, size_t dest_size); CARES_EXTERN ares_bool_t ares_str_isnum(const char *str); CARES_EXTERN ares_bool_t ares_str_isalnum(const char *str); +/*! Parse a base-10 unsigned integer from a NULL-terminated string. Unlike + * atoi(), this rejects overflow, leading or trailing garbage (a sign, + * whitespace, or non-digit characters), and any value that does not fit + * within the caller-provided maximum. + * + * \param[in] str String to parse. May be NULL or empty, in which case + * ARES_FALSE is returned. + * \param[in] max Largest value considered valid. Values above UINT_MAX + * are capped to UINT_MAX since out is an unsigned int. + * \param[out] out On success, receives the parsed value. Untouched on + * failure. + * \return ARES_TRUE if a valid in-range integer was parsed, ARES_FALSE + * otherwise. + */ +CARES_EXTERN ares_bool_t ares_str_parse_uint(const char *str, unsigned long max, + unsigned int *out); + CARES_EXTERN void ares_str_ltrim(char *str); CARES_EXTERN void ares_str_rtrim(char *str); CARES_EXTERN void ares_str_trim(char *str); diff --git a/deps/cares/src/lib/legacy/ares_expand_name.c b/deps/cares/src/lib/legacy/ares_expand_name.c index 72668f4cb60a07..ee1d70076be815 100644 --- a/deps/cares/src/lib/legacy/ares_expand_name.c +++ b/deps/cares/src/lib/legacy/ares_expand_name.c @@ -69,7 +69,7 @@ ares_status_t ares_expand_name_validated(const unsigned char *encoded, } start_len = ares_buf_len(buf); - status = ares_dns_name_parse(buf, s, is_hostname); + status = ares_dns_name_parse(buf, s, is_hostname, ARES_TRUE); if (status != ARES_SUCCESS) { goto done; } diff --git a/deps/cares/src/lib/legacy/ares_parse_ns_reply.c b/deps/cares/src/lib/legacy/ares_parse_ns_reply.c index fc9ab9219d399c..7013ce28b90131 100644 --- a/deps/cares/src/lib/legacy/ares_parse_ns_reply.c +++ b/deps/cares/src/lib/legacy/ares_parse_ns_reply.c @@ -97,12 +97,12 @@ int ares_parse_ns_reply(const unsigned char *abuf, int alen_int, } /* Preallocate the maximum number + 1 */ - hostent->h_aliases = ares_malloc((ancount + 1) * sizeof(*hostent->h_aliases)); + hostent->h_aliases = + ares_malloc_zero_array(ancount + 1, sizeof(*hostent->h_aliases)); if (hostent->h_aliases == NULL) { status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } - memset(hostent->h_aliases, 0, (ancount + 1) * sizeof(*hostent->h_aliases)); for (i = 0; i < ancount; i++) { const ares_dns_rr_t *rr = diff --git a/deps/cares/src/lib/legacy/ares_parse_ptr_reply.c b/deps/cares/src/lib/legacy/ares_parse_ptr_reply.c index 0e52f9db09bbc7..a275f3802ffc08 100644 --- a/deps/cares/src/lib/legacy/ares_parse_ptr_reply.c +++ b/deps/cares/src/lib/legacy/ares_parse_ptr_reply.c @@ -88,12 +88,12 @@ ares_status_t ares_parse_ptr_reply_dnsrec(const ares_dns_record_t *dnsrec, hostent->h_length = (HOSTENT_LENGTH_TYPE)addrlen; /* Preallocate the maximum number + 1 */ - hostent->h_aliases = ares_malloc((ancount + 1) * sizeof(*hostent->h_aliases)); + hostent->h_aliases = + ares_malloc_zero_array(ancount + 1, sizeof(*hostent->h_aliases)); if (hostent->h_aliases == NULL) { status = ARES_ENOMEM; goto done; } - memset(hostent->h_aliases, 0, (ancount + 1) * sizeof(*hostent->h_aliases)); /* Cycle through answers */ diff --git a/deps/cares/src/lib/record/ares_dns_mapping.c b/deps/cares/src/lib/record/ares_dns_mapping.c index 5a3ec28abf130b..38dedd438c6dd5 100644 --- a/deps/cares/src/lib/record/ares_dns_mapping.c +++ b/deps/cares/src/lib/record/ares_dns_mapping.c @@ -147,7 +147,7 @@ ares_bool_t ares_dns_class_isvalid(ares_dns_class_t qclass, switch (qclass) { case ARES_CLASS_IN: case ARES_CLASS_CHAOS: - case ARES_CLASS_HESOID: + case ARES_CLASS_HESIOD: case ARES_CLASS_NONE: return ARES_TRUE; case ARES_CLASS_ANY: @@ -228,7 +228,7 @@ const char *ares_dns_rec_type_tostr(ares_dns_rec_type_t type) case ARES_REC_TYPE_CAA: return "CAA"; case ARES_REC_TYPE_RAW_RR: - return "RAWRR"; + return "RAW_RR"; } return "UNKNOWN"; } @@ -240,7 +240,7 @@ const char *ares_dns_class_tostr(ares_dns_class_t qclass) return "IN"; case ARES_CLASS_CHAOS: return "CH"; - case ARES_CLASS_HESOID: + case ARES_CLASS_HESIOD: return "HS"; case ARES_CLASS_ANY: return "ANY"; @@ -670,7 +670,7 @@ ares_bool_t ares_dns_class_fromstr(ares_dns_class_t *qclass, const char *str) } list[] = { { "IN", ARES_CLASS_IN }, { "CH", ARES_CLASS_CHAOS }, - { "HS", ARES_CLASS_HESOID }, + { "HS", ARES_CLASS_HESIOD }, { "NONE", ARES_CLASS_NONE }, { "ANY", ARES_CLASS_ANY }, { NULL, 0 } diff --git a/deps/cares/src/lib/record/ares_dns_multistring.c b/deps/cares/src/lib/record/ares_dns_multistring.c index 44fcaccd65bb6a..a2a7adc4e4527f 100644 --- a/deps/cares/src/lib/record/ares_dns_multistring.c +++ b/deps/cares/src/lib/record/ares_dns_multistring.c @@ -227,7 +227,9 @@ const unsigned char *ares_dns_multistring_combined(ares_dns_multistring_t *strs, strs->cache_str = (unsigned char *)ares_buf_finish_str(buf, &strs->cache_str_len); - if (strs->cache_str != NULL) { + if (strs->cache_str == NULL) { + ares_buf_destroy(buf); + } else { strs->cache_invalidated = ARES_FALSE; } *len = strs->cache_str_len; diff --git a/deps/cares/src/lib/record/ares_dns_name.c b/deps/cares/src/lib/record/ares_dns_name.c index a9b92e03ca9353..ffcaf5e110d547 100644 --- a/deps/cares/src/lib/record/ares_dns_name.c +++ b/deps/cares/src/lib/record/ares_dns_name.c @@ -25,6 +25,18 @@ */ #include "ares_private.h" +/* RFC 1035 3.1 limits a name to 255 octets. We track presentation length + * (label octets plus one separator before each label after the first), which is + * up to ~2 octets looser than the strict wire limit and so never rejects a + * compliant name. Shared by the read and write paths. */ +#define ARES_MAX_NAME_PRESENTATION_LEN 255 + +/* A name of <= 255 octets holds at most 128 labels, so it can never legitimately + * require more than that many compression-pointer jumps. Bounding the number of + * indirections stops a name built purely from pointers (which never adds label + * bytes, so the length cap never fires) from being walked without limit. */ +#define ARES_MAX_INDIRS 128 + typedef struct { char *name; size_t name_len; @@ -48,10 +60,18 @@ static ares_status_t ares_nameoffset_create(ares_llist_t **list, ares_nameoffset_t *off = NULL; if (list == NULL || name == NULL || ares_strlen(name) == 0 || - ares_strlen(name) > 255) { + ares_strlen(name) > ARES_MAX_NAME_PRESENTATION_LEN) { return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ } + /* Don't record a name as a compression target if it can't be referenced by + * a pointer. DNS name compression offsets are only 14 bits (RFC 1035 + * 4.1.4), so a name positioned beyond 0x3FFF would have its offset + * truncated when written, producing a pointer to the wrong location. */ + if (idx > 0x3FFF) { + return ARES_SUCCESS; + } + if (*list == NULL) { *list = ares_llist_create(ares_nameoffset_free); } @@ -65,7 +85,12 @@ static ares_status_t ares_nameoffset_create(ares_llist_t **list, return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ } - off->name = ares_strdup(name); + off->name = ares_strdup(name); + if (off->name == NULL) { + status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + off->name_len = ares_strlen(off->name); off->idx = idx; @@ -335,7 +360,8 @@ static ares_status_t ares_split_dns_name(ares_array_t *labels, } /* Can't exceed maximum (unescaped) length */ - if (ares_array_len(labels) && total_len + ares_array_len(labels) - 1 > 255) { + if (ares_array_len(labels) && + total_len + ares_array_len(labels) - 1 > ARES_MAX_NAME_PRESENTATION_LEN) { status = ARES_EBADNAME; goto done; } @@ -419,6 +445,9 @@ ares_status_t ares_dns_name_write(ares_buf_t *buf, ares_llist_t **list, /* Output name compression offset jump */ if (off != NULL) { + /* off->idx is guaranteed to fit in 14 bits: ares_nameoffset_create() + * refuses to record offsets beyond 0x3FFF, so the mask below never + * actually truncates. */ unsigned short u16 = (unsigned short)0xC000 | (unsigned short)(off->idx & 0x3FFF); status = ares_buf_append_be16(buf, u16); @@ -531,13 +560,16 @@ static ares_status_t ares_fetch_dnsname_into_buf(ares_buf_t *buf, } ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name, - ares_bool_t is_hostname) + ares_bool_t is_hostname, + ares_bool_t allow_compression) { size_t save_offset = 0; unsigned char c; ares_status_t status; ares_buf_t *namebuf = NULL; size_t label_start = ares_buf_get_position(buf); + size_t name_len = 0; + size_t indir = 0; if (buf == NULL) { return ARES_EFORMERR; @@ -588,6 +620,13 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name, */ size_t offset = (size_t)((c & 0x3F) << 8); + /* Some RR types (e.g. SRV) do not permit name compression within their + * RDATA. Reject a compression pointer when it is not allowed. */ + if (!allow_compression) { + status = ARES_EBADNAME; + goto fail; + } + /* Fetch second byte of the redirect length */ status = ares_buf_fetch_bytes(buf, &c, 1); if (status != ARES_SUCCESS) { @@ -609,6 +648,16 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name, goto fail; } + /* Bound the number of indirections. A name made purely of pointers never + * adds label bytes, so the length cap below can't stop it; the + * strictly-decreasing rule alone still allows thousands of jumps per name. + * No legitimate <= 255 octet name needs more than 128 labels/jumps. */ + indir++; + if (indir > ARES_MAX_INDIRS) { + status = ARES_EBADNAME; + goto fail; + } + /* First time we make a jump, save the current position */ if (save_offset == 0) { save_offset = ares_buf_get_position(buf); @@ -632,6 +681,20 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name, /* New label */ + /* RFC 1035 3.1 limits a name to 255 octets. Enforce it during + * decompression so labels reached through a chain of pointers can't expand a + * single name without bound. Track the presentation length (label data plus + * the separator that precedes each label after the first), matching + * ares_split_dns_name() on the write side. */ + if (name_len) { + name_len++; + } + name_len += c; + if (name_len > ARES_MAX_NAME_PRESENTATION_LEN) { + status = ARES_EBADNAME; + goto fail; + } + /* Labels are separated by periods */ if (ares_buf_len(namebuf) != 0 && name != NULL) { status = ares_buf_append_byte(namebuf, '.'); diff --git a/deps/cares/src/lib/record/ares_dns_parse.c b/deps/cares/src/lib/record/ares_dns_parse.c index 0c545d7aa18ada..9dc28e92d84247 100644 --- a/deps/cares/src/lib/record/ares_dns_parse.c +++ b/deps/cares/src/lib/record/ares_dns_parse.c @@ -46,8 +46,14 @@ static ares_status_t ares_dns_parse_and_set_dns_name(ares_buf_t *buf, { ares_status_t status; char *name = NULL; + /* Only RR types defined in RFC1035 may use name compression within their + * RDATA (RFC3597). Reject compression pointers for any other type (e.g. + * SRV per RFC2782) to match the write-side policy and avoid following + * pointers that a non-understanding nameserver could not have rewritten. */ + ares_bool_t allow_compression = + ares_dns_rec_allow_name_comp(ares_dns_rr_get_type(rr)); - status = ares_dns_name_parse(buf, &name, is_hostname); + status = ares_dns_name_parse(buf, &name, is_hostname, allow_compression); if (status != ARES_SUCCESS) { return status; } @@ -514,6 +520,7 @@ static ares_status_t ares_dns_parse_rr_opt(ares_buf_t *buf, ares_dns_rr_t *rr, status = ares_dns_rr_set_opt_own(rr, ARES_RR_OPT_OPTIONS, opt, val, len); if (status != ARES_SUCCESS) { + ares_free(val); return status; } } @@ -607,6 +614,7 @@ static ares_status_t ares_dns_parse_rr_svcb(ares_buf_t *buf, ares_dns_rr_t *rr, status = ares_dns_rr_set_opt_own(rr, ARES_RR_SVCB_PARAMS, opt, val, len); if (status != ARES_SUCCESS) { + ares_free(val); return status; } } @@ -658,6 +666,7 @@ static ares_status_t ares_dns_parse_rr_https(ares_buf_t *buf, ares_dns_rr_t *rr, status = ares_dns_rr_set_opt_own(rr, ARES_RR_HTTPS_PARAMS, opt, val, len); if (status != ARES_SUCCESS) { + ares_free(val); return status; } } @@ -765,6 +774,12 @@ static ares_status_t ares_dns_parse_rr_raw_rr(ares_buf_t *buf, ares_status_t status; unsigned char *bytes = NULL; + /* Can't fail */ + status = ares_dns_rr_set_u16(rr, ARES_RR_RAW_RR_TYPE, raw_type); + if (status != ARES_SUCCESS) { + return status; + } + if (rdlength == 0) { return ARES_SUCCESS; } @@ -774,13 +789,6 @@ static ares_status_t ares_dns_parse_rr_raw_rr(ares_buf_t *buf, return status; } - /* Can't fail */ - status = ares_dns_rr_set_u16(rr, ARES_RR_RAW_RR_TYPE, raw_type); - if (status != ARES_SUCCESS) { - ares_free(bytes); - return status; - } - status = ares_dns_rr_set_bin_own(rr, ARES_RR_RAW_RR_DATA, bytes, rdlength); if (status != ARES_SUCCESS) { ares_free(bytes); @@ -920,30 +928,6 @@ static ares_status_t ares_dns_parse_header(ares_buf_t *buf, unsigned int flags, (*dnsrec)->raw_rcode = rcode; - if (*ancount > 0) { - status = - ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, *ancount); - if (status != ARES_SUCCESS) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ - } - } - - if (*nscount > 0) { - status = - ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, *nscount); - if (status != ARES_SUCCESS) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ - } - } - - if (*arcount > 0) { - status = - ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, *arcount); - if (status != ARES_SUCCESS) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ - } - } - return ARES_SUCCESS; fail: @@ -1032,7 +1016,7 @@ static ares_status_t ares_dns_parse_qd(ares_buf_t *buf, */ /* Name */ - status = ares_dns_name_parse(buf, &name, ARES_FALSE); + status = ares_dns_name_parse(buf, &name, ARES_FALSE, ARES_TRUE); if (status != ARES_SUCCESS) { goto done; } @@ -1103,7 +1087,7 @@ static ares_status_t ares_dns_parse_rr(ares_buf_t *buf, unsigned int flags, */ /* Name */ - status = ares_dns_name_parse(buf, &name, ARES_FALSE); + status = ares_dns_name_parse(buf, &name, ARES_FALSE, ARES_TRUE); if (status != ARES_SUCCESS) { goto done; } @@ -1208,6 +1192,8 @@ static ares_status_t ares_dns_parse_buf(ares_buf_t *buf, unsigned int flags, ares_dns_record_t **dnsrec) { ares_status_t status; + size_t total_rr_count; + const size_t min_rr_wire_len = 11; unsigned short qdcount; unsigned short ancount; unsigned short nscount; @@ -1268,6 +1254,35 @@ static ares_status_t ares_dns_parse_buf(ares_buf_t *buf, unsigned int flags, } } + total_rr_count = (size_t)ancount + (size_t)nscount + (size_t)arcount; + if (total_rr_count > ares_buf_len(buf) / min_rr_wire_len) { + status = ARES_EBADRESP; + goto fail; + } + + if (ancount > 0) { + status = ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, ancount); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + if (nscount > 0) { + status = + ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, nscount); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + if (arcount > 0) { + status = + ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, arcount); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + /* Parse Answers */ for (i = 0; i < ancount; i++) { status = ares_dns_parse_rr(buf, flags, ARES_SECTION_ANSWER, *dnsrec); @@ -1292,6 +1307,22 @@ static ares_status_t ares_dns_parse_buf(ares_buf_t *buf, unsigned int flags, } } + /* RFC 6891 6.1.1: a message MUST NOT contain more than one OPT RR. Reject + * any response carrying multiple OPT records in the additional section. */ + { + size_t rr_cnt = ares_dns_record_rr_cnt(*dnsrec, ARES_SECTION_ADDITIONAL); + size_t opt_cnt = 0; + size_t j; + for (j = 0; j < rr_cnt; j++) { + const ares_dns_rr_t *rr = + ares_dns_record_rr_get_const(*dnsrec, ARES_SECTION_ADDITIONAL, j); + if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT && ++opt_cnt > 1) { + status = ARES_EBADRESP; + goto fail; + } + } + } + /* Finalize rcode now that if we have OPT it is processed */ if (!ares_dns_rcode_isvalid((*dnsrec)->raw_rcode)) { (*dnsrec)->rcode = ARES_RCODE_SERVFAIL; diff --git a/deps/cares/src/lib/record/ares_dns_record.c b/deps/cares/src/lib/record/ares_dns_record.c index ec0dfbd13c49f3..2c0dadd084ca66 100644 --- a/deps/cares/src/lib/record/ares_dns_record.c +++ b/deps/cares/src/lib/record/ares_dns_record.c @@ -929,7 +929,7 @@ ares_status_t ares_dns_rr_add_abin(ares_dns_rr_t *dns_rr, ares_dns_rr_key_t key, ares_dns_datatype_t datatype = ares_dns_rr_key_datatype(key); ares_bool_t is_nullterm = (datatype == ARES_DATATYPE_ABINP) ? ARES_TRUE : ARES_FALSE; - size_t alloclen = is_nullterm ? len + 1 : len; + size_t alloclen; unsigned char *temp; ares_dns_multistring_t **strs; @@ -937,6 +937,15 @@ ares_status_t ares_dns_rr_add_abin(ares_dns_rr_t *dns_rr, ares_dns_rr_key_t key, return ARES_EFORMERR; } + if (val == NULL && len != 0) { + return ARES_EFORMERR; + } + + if (is_nullterm && len == SIZE_MAX) { + return ARES_ENOMEM; + } + alloclen = is_nullterm ? len + 1 : len; + strs = ares_dns_rr_data_ptr(dns_rr, key, NULL); if (strs == NULL) { return ARES_EFORMERR; @@ -954,7 +963,9 @@ ares_status_t ares_dns_rr_add_abin(ares_dns_rr_t *dns_rr, ares_dns_rr_key_t key, return ARES_ENOMEM; } - memcpy(temp, val, len); + if (len != 0) { + memcpy(temp, val, len); + } /* NULL-term ABINP */ if (is_nullterm) { @@ -1237,14 +1248,32 @@ ares_status_t ares_dns_rr_set_bin(ares_dns_rr_t *dns_rr, ares_dns_rr_key_t key, (datatype == ARES_DATATYPE_BINP || datatype == ARES_DATATYPE_ABINP) ? ARES_TRUE : ARES_FALSE; - size_t alloclen = is_nullterm ? len + 1 : len; - unsigned char *temp = ares_malloc(alloclen); + size_t alloclen; + unsigned char *temp; + + if (datatype != ARES_DATATYPE_BIN && datatype != ARES_DATATYPE_BINP && + datatype != ARES_DATATYPE_ABINP) { + return ARES_EFORMERR; + } + + if (val == NULL && len != 0) { + return ARES_EFORMERR; + } + + if (is_nullterm && len == SIZE_MAX) { + return ARES_ENOMEM; + } + alloclen = is_nullterm ? len + 1 : len; + + temp = ares_malloc(alloclen); if (temp == NULL) { return ARES_ENOMEM; } - memcpy(temp, val, len); + if (len != 0) { + memcpy(temp, val, len); + } /* NULL-term BINP */ if (is_nullterm) { @@ -1400,12 +1429,23 @@ ares_status_t ares_dns_rr_set_opt(ares_dns_rr_t *dns_rr, ares_dns_rr_key_t key, ares_status_t status; if (val != NULL) { - temp = ares_malloc(val_len + 1); + size_t alloclen; + + if (val_len == SIZE_MAX) { + return ARES_ENOMEM; + } + alloclen = val_len + 1; + + temp = ares_malloc(alloclen); if (temp == NULL) { return ARES_ENOMEM; } - memcpy(temp, val, val_len); + if (val_len != 0) { + memcpy(temp, val, val_len); + } temp[val_len] = 0; + } else if (val_len != 0) { + return ARES_EFORMERR; } status = ares_dns_rr_set_opt_own(dns_rr, key, opt, temp, val_len); diff --git a/deps/cares/src/lib/str/ares_buf.c b/deps/cares/src/lib/str/ares_buf.c index 63acc6cf7714d3..25f0372e4dbdcc 100644 --- a/deps/cares/src/lib/str/ares_buf.c +++ b/deps/cares/src/lib/str/ares_buf.c @@ -139,6 +139,7 @@ static ares_status_t ares_buf_ensure_space(ares_buf_t *buf, size_t needed_size) { size_t remaining_size; size_t alloc_size; + size_t total_required; unsigned char *ptr; if (buf == NULL) { @@ -151,9 +152,20 @@ static ares_status_t ares_buf_ensure_space(ares_buf_t *buf, size_t needed_size) /* When calling ares_buf_finish_str() we end up adding a null terminator, * so we want to ensure the size is always sufficient for this as we don't - * want an ARES_ENOMEM at that point */ + * want an ARES_ENOMEM at that point. + */ + if (buf->data_len >= SIZE_MAX - 1) { + return ARES_ENOMEM; + } + needed_size++; + if (needed_size > SIZE_MAX - buf->data_len) { + return ARES_ENOMEM; + } + + total_required = buf->data_len + needed_size; + /* No need to do an expensive move operation, we have enough to just append */ remaining_size = buf->alloc_buf_len - buf->data_len; if (remaining_size >= needed_size) { @@ -177,9 +189,11 @@ static ares_status_t ares_buf_ensure_space(ares_buf_t *buf, size_t needed_size) /* Increase allocation by powers of 2 */ do { - alloc_size <<= 1; - remaining_size = alloc_size - buf->data_len; - } while (remaining_size < needed_size); + if (alloc_size > SIZE_MAX >> 1) { + return ARES_ENOMEM; + } + alloc_size <<= 1; + } while (alloc_size < total_required); ptr = ares_realloc(buf->alloc_buf, alloc_size); if (ptr == NULL) { @@ -1111,7 +1125,7 @@ ares_status_t ares_buf_replace(ares_buf_t *buf, const unsigned char *srch, size_t processed_len = 0; ares_status_t status; - if (buf->alloc_buf == NULL || srch == NULL || srch_size == 0 || + if (buf == NULL || buf->alloc_buf == NULL || srch == NULL || srch_size == 0 || (rplc == NULL && rplc_size != 0)) { return ARES_EFORMERR; } @@ -1131,7 +1145,7 @@ ares_status_t ares_buf_replace(ares_buf_t *buf, const unsigned char *srch, /* Store the offset this was found because our actual pointer might be * switched out from under us by the call to ensure_space() if the * replacement pattern is larger than the search pattern */ - found_offset = (size_t)(ptr - (size_t)(buf->alloc_buf + buf->offset)); + found_offset = (size_t)(ptr - buf->alloc_buf) - buf->offset; if (rplc_size > srch_size) { status = ares_buf_ensure_space(buf, rplc_size - srch_size); if (status != ARES_SUCCESS) { @@ -1262,17 +1276,15 @@ static ares_status_t } done: - if (status != ARES_SUCCESS) { - ares_buf_destroy(binbuf); + if (status == ARES_SUCCESS && bin != NULL) { + size_t mylen = 0; + /* NOTE: we use ares_buf_finish_str() here as we guarantee NULL + * Termination even though we are technically returning binary data. + */ + *bin = (unsigned char *)ares_buf_finish_str(binbuf, &mylen); + *bin_len = mylen; } else { - if (bin != NULL) { - size_t mylen = 0; - /* NOTE: we use ares_buf_finish_str() here as we guarantee NULL - * Termination even though we are technically returning binary data. - */ - *bin = (unsigned char *)ares_buf_finish_str(binbuf, &mylen); - *bin_len = mylen; - } + ares_buf_destroy(binbuf); } return status; diff --git a/deps/cares/src/lib/str/ares_str.c b/deps/cares/src/lib/str/ares_str.c index 0eda1ab9f15783..0819dc57849049 100644 --- a/deps/cares/src/lib/str/ares_str.c +++ b/deps/cares/src/lib/str/ares_str.c @@ -28,6 +28,9 @@ #include "ares_private.h" #include "ares_str.h" +#include +#include + #ifdef HAVE_STDINT_H # include #endif @@ -125,6 +128,57 @@ ares_bool_t ares_str_isnum(const char *str) return ARES_TRUE; } +ares_bool_t ares_str_parse_uint(const char *str, unsigned long max, + unsigned int *out) +{ + char *end = NULL; + unsigned long val; + + /* Require a leading digit so strtoul()'s tolerance of a sign or leading + * whitespace (e.g. "-1" wrapping to UINT_MAX) can't slip through here. This + * also rejects NULL and empty strings. */ + if (str == NULL || out == NULL || !ares_isdigit(*str)) { + return ARES_FALSE; + } + + /* out is unsigned int, so a max above UINT_MAX would let strtoul's result + * truncate silently on assignment. Cap it. */ + if (max > UINT_MAX) { + max = UINT_MAX; + } + + errno = 0; + val = strtoul(str, &end, 10); + if (errno == ERANGE || *end != '\0' || val > max) { + return ARES_FALSE; + } + + *out = (unsigned int)val; + return ARES_TRUE; +} + +ares_bool_t ares_parse_port(const char *str, unsigned short *port, + ares_bool_t allow_zero) +{ + unsigned int val; + + if (port == NULL) { + return ARES_FALSE; + } + + if (!ares_str_parse_uint(str, 65535UL, &val)) { + return ARES_FALSE; + } + + if (!allow_zero && val == 0) { + return ARES_FALSE; + } + + *port = (unsigned short)val; + + return ARES_TRUE; +} + ares_bool_t ares_str_isalnum(const char *str) { size_t i; diff --git a/deps/cares/src/lib/util/ares_iface_ips.c b/deps/cares/src/lib/util/ares_iface_ips.c index c5f507f87e1476..66d1fba84d4bce 100644 --- a/deps/cares/src/lib/util/ares_iface_ips.c +++ b/deps/cares/src/lib/util/ares_iface_ips.c @@ -25,6 +25,8 @@ */ #include "ares_private.h" +#include + #ifdef USE_WINSOCK # include # include @@ -256,12 +258,12 @@ ares_iface_ip_flags_t ares_iface_ips_get_flags(const ares_iface_ips_t *ips, const ares_iface_ip_t *ip; if (ips == NULL) { - return 0; + return ARES_IFACE_IP_NONE; } ip = ares_array_at_const(ips->ips, idx); if (ip == NULL) { - return 0; + return ARES_IFACE_IP_NONE; } return ip->flags; @@ -329,6 +331,8 @@ static char *wcharp_to_charp(const wchar_t *in) static ares_bool_t name_match(const char *name, const char *adapter_name, unsigned int ll_scope) { + unsigned int scope; + if (name == NULL || *name == 0) { return ARES_TRUE; } @@ -337,7 +341,7 @@ static ares_bool_t name_match(const char *name, const char *adapter_name, return ARES_TRUE; } - if (ares_str_isnum(name) && (unsigned int)atoi(name) == ll_scope) { + if (ares_str_parse_uint(name, UINT_MAX, &scope) && scope == ll_scope) { return ARES_TRUE; } @@ -376,7 +380,7 @@ static ares_status_t ares_iface_ips_enumerate(ares_iface_ips_t *ips, for (address = addresses; address != NULL; address = address->Next) { IP_ADAPTER_UNICAST_ADDRESS *ipaddr = NULL; - ares_iface_ip_flags_t addrflag = 0; + ares_iface_ip_flags_t addrflag = ARES_IFACE_IP_NONE; char ifname[64] = ""; # if defined(HAVE_CONVERTINTERFACEINDEXTOLUID) && \ @@ -477,7 +481,7 @@ static ares_status_t ares_iface_ips_enumerate(ares_iface_ips_t *ips, } for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) { - ares_iface_ip_flags_t addrflag = 0; + ares_iface_ip_flags_t addrflag = ARES_IFACE_IP_NONE; struct ares_addr addr; unsigned char netmask = 0; unsigned int ll_scope = 0; @@ -500,20 +504,28 @@ static ares_status_t ares_iface_ips_enumerate(ares_iface_ips_t *ips, addr.family = AF_INET; memcpy(&addr.addr.addr4, &sockaddr_in->sin_addr, sizeof(addr.addr.addr4)); /* netmask */ - sockaddr_in = (struct sockaddr_in *)((void *)ifa->ifa_netmask); - netmask = count_addr_bits((const void *)&sockaddr_in->sin_addr, 4); + if (ifa->ifa_netmask != NULL) { + sockaddr_in = (struct sockaddr_in *)((void *)ifa->ifa_netmask); + netmask = count_addr_bits((const void *)&sockaddr_in->sin_addr, 4); + } else { + netmask = 32; + } } else if (ifa->ifa_addr->sa_family == AF_INET6) { const struct sockaddr_in6 *sockaddr_in6 = (const struct sockaddr_in6 *)((void *)ifa->ifa_addr); addr.family = AF_INET6; memcpy(&addr.addr.addr6, &sockaddr_in6->sin6_addr, sizeof(addr.addr.addr6)); - /* netmask */ - sockaddr_in6 = (struct sockaddr_in6 *)((void *)ifa->ifa_netmask); - netmask = count_addr_bits((const void *)&sockaddr_in6->sin6_addr, 16); # ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID ll_scope = sockaddr_in6->sin6_scope_id; # endif + /* netmask */ + if (ifa->ifa_netmask != NULL) { + sockaddr_in6 = (struct sockaddr_in6 *)((void *)ifa->ifa_netmask); + netmask = count_addr_bits((const void *)&sockaddr_in6->sin6_addr, 16); + } else { + netmask = 128; + } } else { /* unknown */ continue; diff --git a/deps/cares/src/lib/util/ares_iface_ips.h b/deps/cares/src/lib/util/ares_iface_ips.h index f22e09046a065b..ec2353448fb0f5 100644 --- a/deps/cares/src/lib/util/ares_iface_ips.h +++ b/deps/cares/src/lib/util/ares_iface_ips.h @@ -28,6 +28,7 @@ /*! Flags for interface ip addresses. */ typedef enum { + ARES_IFACE_IP_NONE = 0, /*!< No flags set / unspecified */ ARES_IFACE_IP_V4 = 1 << 0, /*!< IPv4 address. During enumeration if * this flag is set ARES_IFACE_IP_V6 * is not, will only enumerate v4 diff --git a/deps/cares/src/lib/util/ares_math.c b/deps/cares/src/lib/util/ares_math.c index 1106bf6bf151f8..3513146c3ddc4a 100644 --- a/deps/cares/src/lib/util/ares_math.c +++ b/deps/cares/src/lib/util/ares_math.c @@ -42,7 +42,7 @@ static unsigned int ares_round_up_pow2_u32(unsigned int n) return n; } -static ares_int64_t ares_round_up_pow2_u64(ares_int64_t n) +static ares_uint64_t ares_round_up_pow2_u64(ares_uint64_t n) { /* NOTE: if already a power of 2, will return itself, not the next */ n--; @@ -73,7 +73,7 @@ ares_bool_t ares_is_64bit(void) size_t ares_round_up_pow2(size_t n) { if (ares_is_64bit()) { - return (size_t)ares_round_up_pow2_u64((ares_int64_t)n); + return (size_t)ares_round_up_pow2_u64((ares_uint64_t)n); } return (size_t)ares_round_up_pow2_u32((unsigned int)n); @@ -156,3 +156,12 @@ unsigned char ares_count_bits_u8(unsigned char x) static const unsigned char lookup[256] = { B6(0), B6(1), B6(1), B6(2) }; return lookup[x]; } + +ares_bool_t ares_size_t_mul_overflow(size_t a, size_t b, size_t *res) +{ + if (a > 0 && b > SIZE_MAX / a) { + return ARES_TRUE; + } + *res = a * b; + return ARES_FALSE; +} diff --git a/deps/cares/src/lib/util/ares_math.h b/deps/cares/src/lib/util/ares_math.h index 3b60b00bdbb6ce..76e1e488370984 100644 --- a/deps/cares/src/lib/util/ares_math.h +++ b/deps/cares/src/lib/util/ares_math.h @@ -50,4 +50,9 @@ size_t ares_count_digits(size_t n); size_t ares_count_hexdigits(size_t n); unsigned char ares_count_bits_u8(unsigned char x); +/*! Multiply two size_t values, checking for overflow. On success writes the + * product to *res and returns ARES_FALSE. On overflow returns ARES_TRUE and + * leaves *res untouched. */ +ares_bool_t ares_size_t_mul_overflow(size_t a, size_t b, size_t *res); + #endif diff --git a/deps/cares/src/lib/util/ares_threads.c b/deps/cares/src/lib/util/ares_threads.c index ab0b51afb70577..b27b7ebd70005f 100644 --- a/deps/cares/src/lib/util/ares_threads.c +++ b/deps/cares/src/lib/util/ares_threads.c @@ -563,8 +563,7 @@ ares_status_t ares_queue_wait_empty(ares_channel_t *channel, int timeout_ms) if (timeout_ms >= 0) { ares_tvnow(&tout); - tout.sec += (ares_int64_t)(timeout_ms / 1000); - tout.usec += (unsigned int)(timeout_ms % 1000) * 1000; + ares_timeval_add(&tout, (size_t)timeout_ms); } ares_thread_mutex_lock(channel->lock); diff --git a/deps/cares/src/lib/util/ares_time.h b/deps/cares/src/lib/util/ares_time.h index c6eaf97366379e..ecb721de188719 100644 --- a/deps/cares/src/lib/util/ares_time.h +++ b/deps/cares/src/lib/util/ares_time.h @@ -39,6 +39,7 @@ ares_bool_t ares_timedout(const ares_timeval_t *now, const ares_timeval_t *check); void ares_tvnow(ares_timeval_t *now); +void ares_timeval_add(ares_timeval_t *now, size_t millisecs); void ares_timeval_remaining(ares_timeval_t *remaining, const ares_timeval_t *now, const ares_timeval_t *tout); diff --git a/deps/cares/src/lib/util/ares_uri.c b/deps/cares/src/lib/util/ares_uri.c index 04bad0074a79e2..97f3f3f542a7cf 100644 --- a/deps/cares/src/lib/util/ares_uri.c +++ b/deps/cares/src/lib/util/ares_uri.c @@ -1174,6 +1174,7 @@ static ares_status_t ares_uri_parse_hostport(ares_uri_t *uri, ares_buf_t *buf) unsigned char b; char host[256]; char port[6]; + unsigned short parsed_port; size_t len; ares_status_t status; @@ -1242,11 +1243,11 @@ static ares_status_t ares_uri_parse_hostport(ares_uri_t *uri, ares_buf_t *buf) } port[len] = 0; - if (!ares_str_isnum(port)) { + if (!ares_parse_port(port, &parsed_port, ARES_TRUE)) { return ARES_EBADSTR; } - status = ares_uri_set_port(uri, (unsigned short)atoi(port)); + status = ares_uri_set_port(uri, parsed_port); if (status != ARES_SUCCESS) { return status; } diff --git a/deps/cares/src/tools/adig.c b/deps/cares/src/tools/adig.c index fce210a8053578..29f359a39287c1 100644 --- a/deps/cares/src/tools/adig.c +++ b/deps/cares/src/tools/adig.c @@ -1162,12 +1162,12 @@ static ares_bool_t read_cmdline(int argc, const char * const *argv, /* skip prefix */ if (dig_options[opt].prefix != 0) { nameptr++; - } - - /* Negated option if it has a 'no' prefix */ - if (ares_streq_max(nameptr, "no", 2)) { - is_true = ARES_FALSE; - nameptr += 2; + /* Negated option if it has a 'no' prefix */ + if (dig_options[opt].prefix == '+' && + ares_streq_max(nameptr, "no", 2)) { + is_true = ARES_FALSE; + nameptr += 2; + } } if (dig_options[opt].separator != 0) { diff --git a/deps/cares/src/tools/ahost.c b/deps/cares/src/tools/ahost.c index 7d1d4a86dc7a2d..67b5677cad6e25 100644 --- a/deps/cares/src/tools/ahost.c +++ b/deps/cares/src/tools/ahost.c @@ -44,6 +44,13 @@ #include "ares_str.h" +#define RV_OK 0 /* Success */ +#define RV_SYSERR 1 /* Internal system failure */ +#define RV_MISUSE 2 /* Misuse (command line) */ +#define RV_FAIL 3 /* Resolution failure */ +static int final_rv = RV_OK; + + static void callback(void *arg, int status, int timeouts, struct hostent *host); static void ai_callback(void *arg, int status, int timeouts, struct ares_addrinfo *result); diff --git a/deps/ncrypto/engine.cc b/deps/ncrypto/engine.cc index 1845cfcca47aa4..a8e64e25049183 100644 --- a/deps/ncrypto/engine.cc +++ b/deps/ncrypto/engine.cc @@ -1,12 +1,18 @@ #include "ncrypto.h" +#if !defined(OPENSSL_NO_ENGINE) && \ + ((defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT) || \ + NCRYPTO_USE_LEGACY_OPENSSL) +#include +#endif + namespace ncrypto { // ============================================================================ // Engine #ifndef OPENSSL_NO_ENGINE -EnginePointer::EnginePointer(ENGINE* engine_, bool finish_on_exit_) +EnginePointer::EnginePointer(void* engine_, bool finish_on_exit_) : engine(engine_), finish_on_exit(finish_on_exit_) {} EnginePointer::EnginePointer(EnginePointer&& other) noexcept @@ -24,21 +30,22 @@ EnginePointer& EnginePointer::operator=(EnginePointer&& other) noexcept { return *new (this) EnginePointer(std::move(other)); } -void EnginePointer::reset(ENGINE* engine_, bool finish_on_exit_) { +void EnginePointer::reset(void* engine_, bool finish_on_exit_) { if (engine != nullptr) { + ENGINE* current = static_cast(engine); if (finish_on_exit) { // This also does the equivalent of ENGINE_free. - ENGINE_finish(engine); + ENGINE_finish(current); } else { - ENGINE_free(engine); + ENGINE_free(current); } } engine = engine_; finish_on_exit = finish_on_exit_; } -ENGINE* EnginePointer::release() { - ENGINE* ret = engine; +void* EnginePointer::release() { + void* ret = engine; engine = nullptr; finish_on_exit = false; return ret; @@ -52,8 +59,9 @@ EnginePointer EnginePointer::getEngineByName(const char* name, // Engine not found, try loading dynamically. engine = EnginePointer(ENGINE_by_id("dynamic")); if (engine) { - if (!ENGINE_ctrl_cmd_string(engine.get(), "SO_PATH", name, 0) || - !ENGINE_ctrl_cmd_string(engine.get(), "LOAD", nullptr, 0)) { + ENGINE* current = static_cast(engine.engine); + if (!ENGINE_ctrl_cmd_string(current, "SO_PATH", name, 0) || + !ENGINE_ctrl_cmd_string(current, "LOAD", nullptr, 0)) { engine.reset(); } } @@ -64,19 +72,24 @@ EnginePointer EnginePointer::getEngineByName(const char* name, bool EnginePointer::setAsDefault(uint32_t flags, CryptoErrorList* errors) { if (engine == nullptr) return false; ClearErrorOnReturn clear_error_on_return(errors); - return ENGINE_set_default(engine, flags) != 0; + return ENGINE_set_default(static_cast(engine), flags) != 0; } bool EnginePointer::init(bool finish_on_exit) { if (engine == nullptr) return false; if (finish_on_exit) setFinishOnExit(); - return ENGINE_init(engine) == 1; + return ENGINE_init(static_cast(engine)) == 1; } EVPKeyPointer EnginePointer::loadPrivateKey(const char* key_name) { if (engine == nullptr) return EVPKeyPointer(); - return EVPKeyPointer( - ENGINE_load_private_key(engine, key_name, nullptr, nullptr)); + return EVPKeyPointer(ENGINE_load_private_key( + static_cast(engine), key_name, nullptr, nullptr)); +} + +bool EnginePointer::setClientCertEngine(SSL_CTX* ctx) { + if (engine == nullptr || ctx == nullptr) return false; + return SSL_CTX_set_client_cert_engine(ctx, static_cast(engine)) == 1; } void EnginePointer::initEnginesOnce() { diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index d62485e626c158..771589ed69b421 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -14,6 +14,7 @@ #endif #include #include +#include #include #include #if OPENSSL_VERSION_MAJOR >= 3 @@ -75,9 +76,209 @@ using BignumCtxPointer = DeleteFnPtr; using BignumGenCallbackPointer = DeleteFnPtr; using NetscapeSPKIPointer = DeleteFnPtr; +const EVP_CIPHER* GetCipherCtxCipher(const EVP_CIPHER_CTX* ctx) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return EVP_CIPHER_CTX_get0_cipher(ctx); +#else + return EVP_CIPHER_CTX_cipher(ctx); +#endif +} + +const EVP_MD* GetDigestCtxMd(const EVP_MD_CTX* ctx) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER || NCRYPTO_USE_BORINGSSL + return EVP_MD_CTX_get0_md(ctx); +#else + return EVP_MD_CTX_md(ctx); +#endif +} + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +using OSSLParamBldPointer = DeleteFnPtr; +using OSSLParamPointer = DeleteFnPtr; +struct OpenSSLBufferDeleter { + void operator()(unsigned char* pointer) const { OPENSSL_free(pointer); } +}; +using OpenSSLBufferPointer = + std::unique_ptr; +#endif + static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; +#if NCRYPTO_USE_OPENSSL3_PROVIDER +bool GetPKeyBnParam(const EVP_PKEY* pkey, + const char* name, + DeleteFnPtr* out) { + BIGNUM* bn = nullptr; + if (pkey == nullptr) return false; + if (EVP_PKEY_get_bn_param(pkey, name, &bn) == 1) { + out->reset(bn); + return true; + } + + size_t len = 0; + if (EVP_PKEY_get_octet_string_param(pkey, name, nullptr, 0, &len) != 1) { + return false; + } + auto data = DataPointer::Alloc(len); + if (!data || + EVP_PKEY_get_octet_string_param(pkey, + name, + static_cast(data.get()), + data.size(), + &len) != 1) { + return false; + } + bn = BN_bin2bn(static_cast(data.get()), len, nullptr); + if (bn == nullptr) return false; + out->reset(bn); + return true; +} + +bool GetOptionalPKeyBnParam(const EVP_PKEY* pkey, + const char* name, + DeleteFnPtr* out) { + BIGNUM* bn = nullptr; + if (pkey == nullptr) { + out->reset(); + return true; + } + if (EVP_PKEY_get_bn_param(pkey, name, &bn) == 1) { + out->reset(bn); + return true; + } + + size_t len = 0; + if (EVP_PKEY_get_octet_string_param(pkey, name, nullptr, 0, &len) == 1) { + auto data = DataPointer::Alloc(len); + if (!data || + EVP_PKEY_get_octet_string_param(pkey, + name, + static_cast(data.get()), + data.size(), + &len) != 1) { + return false; + } + bn = BN_bin2bn(static_cast(data.get()), len, nullptr); + if (bn == nullptr) return false; + out->reset(bn); + return true; + } + + out->reset(); + return true; +} + +EVPKeyPointer NewPKeyFromData(int id, int selection, OSSL_PARAM* params) { + auto ctx = EVPKeyCtxPointer::NewFromID(id); + if (!ctx || EVP_PKEY_fromdata_init(ctx.get()) != 1) return {}; + + EVP_PKEY* pkey = nullptr; + if (EVP_PKEY_fromdata(ctx.get(), &pkey, selection, params) != 1) { + return {}; + } + return EVPKeyPointer(pkey); +} + +EVPKeyPointer NewDhPKey(const BIGNUM* p, + const BIGNUM* g, + const BIGNUM* pub = nullptr, + const BIGNUM* priv = nullptr) { + if (p == nullptr || g == nullptr) return {}; + + OSSLParamBldPointer bld(OSSL_PARAM_BLD_new()); + if (!bld || + OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_P, p) != 1 || + OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_G, g) != 1) { + return {}; + } + + int selection = EVP_PKEY_KEY_PARAMETERS; + if (pub != nullptr) { + if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PUB_KEY, pub) != 1) { + return {}; + } + selection |= EVP_PKEY_PUBLIC_KEY; + } + if (priv != nullptr) { + if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PRIV_KEY, priv) != + 1) { + return {}; + } + selection |= EVP_PKEY_PRIVATE_KEY; + } + + OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); + if (!params) return {}; + return NewPKeyFromData(EVP_PKEY_DH, selection, params.get()); +} + +EVPKeyPointer NewDhPKey(const char* group_name, + const BIGNUM* pub = nullptr, + const BIGNUM* priv = nullptr) { + if (group_name == nullptr) return {}; + + if (pub == nullptr && priv == nullptr) { + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new_from_name(nullptr, "DH", nullptr)); + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string( + OSSL_PKEY_PARAM_GROUP_NAME, const_cast(group_name), 0), + OSSL_PARAM_END, + }; + if (!ctx || !ctx.initForParamgen() || + EVP_PKEY_CTX_set_params(ctx.get(), params) != 1) { + return {}; + } + return ctx.paramgen(); + } + + OSSLParamBldPointer bld(OSSL_PARAM_BLD_new()); + if (!bld || OSSL_PARAM_BLD_push_utf8_string( + bld.get(), OSSL_PKEY_PARAM_GROUP_NAME, group_name, 0) != 1) { + return {}; + } + + int selection = EVP_PKEY_KEY_PARAMETERS; + if (pub != nullptr) { + if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PUB_KEY, pub) != 1) { + return {}; + } + selection |= EVP_PKEY_PUBLIC_KEY; + } + if (priv != nullptr) { + if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PRIV_KEY, priv) != + 1) { + return {}; + } + selection |= EVP_PKEY_PRIVATE_KEY; + } + + OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); + if (!params) return {}; + return NewPKeyFromData(EVP_PKEY_DH, selection, params.get()); +} + +bool GetDhParams(const EVP_PKEY* pkey, + DeleteFnPtr* p, + DeleteFnPtr* g, + DeleteFnPtr* q = nullptr, + DeleteFnPtr* j = nullptr) { + return GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_P, p) && + GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_G, g) && + (q == nullptr || + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_Q, q)) && + (j == nullptr || + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_COFACTOR, j)); +} + +bool GetDhKeys(const EVP_PKEY* pkey, + DeleteFnPtr* pub, + DeleteFnPtr* priv) { + return GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_PUB_KEY, pub) && + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_PRIV_KEY, priv); +} +#endif + #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK struct BoringSSLCipher { const EVP_CIPHER* (*get)(); @@ -500,7 +701,18 @@ int BignumPointer::isPrime(int nchecks, }, &innerCb); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return BN_check_prime(get(), ctx.get(), cb.get()); +#elif NCRYPTO_USE_BORINGSSL + int is_probably_prime = 0; + if (BN_primality_test( + &is_probably_prime, get(), nchecks, ctx.get(), 0, cb.get()) != 1) { + return -1; + } + return is_probably_prime; +#else return BN_is_prime_ex(get(), nchecks, ctx.get(), cb.get()); +#endif } BignumPointer BignumPointer::NewPrime(const PrimeConfig& params, @@ -1389,7 +1601,11 @@ bool X509View::ifRsa(KeyCallback callback) const { OSSL3_CONST EVP_PKEY* pkey = X509_get0_pubkey(cert_); auto id = EVP_PKEY_id(pkey); if (id == EVP_PKEY_RSA || id == EVP_PKEY_RSA2 || id == EVP_PKEY_RSA_PSS) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + Rsa rsa(pkey); +#else Rsa rsa(EVP_PKEY_get0_RSA(pkey)); +#endif if (!rsa) [[unlikely]] return true; return callback(rsa); @@ -1402,7 +1618,11 @@ bool X509View::ifEc(KeyCallback callback) const { OSSL3_CONST EVP_PKEY* pkey = X509_get0_pubkey(cert_); auto id = EVP_PKEY_id(pkey); if (id == EVP_PKEY_EC) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + Ec ec(pkey); +#else Ec ec(EVP_PKEY_get0_EC_KEY(pkey)); +#endif if (!ec) [[unlikely]] return true; return callback(ec); @@ -1430,7 +1650,11 @@ X509Pointer X509Pointer::IssuerFrom(const SSL_CTX* ctx, const X509View& cert) { } X509Pointer X509Pointer::PeerFrom(const SSLPointer& ssl) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return X509Pointer(SSL_get1_peer_certificate(ssl.get())); +#else return X509Pointer(SSL_get_peer_certificate(ssl.get())); +#endif } // When adding or removing errors below, please also update the list in the API @@ -1560,11 +1784,198 @@ bool EqualNoCase(const std::string_view a, const std::string_view b) { return std::tolower(a) == std::tolower(b); }); } + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +const char* GetOpenSSLDhGroupName(const std::string_view name, + DHPointer::FindGroupOption option) { + if (option != DHPointer::FindGroupOption::NO_SMALL_PRIMES && + EqualNoCase(name, "modp5")) { + return "modp_1536"; + } + if (EqualNoCase(name, "modp14")) return "modp_2048"; + if (EqualNoCase(name, "modp15")) return "modp_3072"; + if (EqualNoCase(name, "modp16")) return "modp_4096"; + if (EqualNoCase(name, "modp17")) return "modp_6144"; + if (EqualNoCase(name, "modp18")) return "modp_8192"; + return nullptr; +} + +int GetDhGroupPrivateBits(const char* group_name) { + if (group_name == nullptr) return 0; + if (strcmp(group_name, "modp_1536") == 0) return 200; + if (strcmp(group_name, "modp_2048") == 0) return 225; + if (strcmp(group_name, "modp_3072") == 0) return 275; + if (strcmp(group_name, "modp_4096") == 0) return 325; + if (strcmp(group_name, "modp_6144") == 0) return 375; + if (strcmp(group_name, "modp_8192") == 0) return 400; + return 0; +} + +bool GenerateDhPrivateKey(BignumPointer* out, + const BIGNUM* p, + const char* group_name) { + if (out == nullptr || p == nullptr) return false; + auto priv = BignumPointer::NewSecure(); + if (!priv) return false; + + const int bits = GetDhGroupPrivateBits(group_name); + if (bits > 0) { + if (BN_priv_rand(priv.get(), bits, BN_RAND_TOP_ONE, BN_RAND_BOTTOM_ANY) != + 1) { + return false; + } + } else { + auto range = BignumPointer(BN_dup(p)); + if (!range || BN_sub_word(range.get(), 3) != 1 || + BN_priv_rand_range(priv.get(), range.get()) != 1 || + BN_add_word(priv.get(), 2) != 1) { + return false; + } + } + + *out = std::move(priv); + return true; +} + +// Recompute DH public keys locally when a private key already exists. Provider +// keygen creates a fresh keypair, which is both slower and changes semantics. +bool GenerateDhPublicKey(BignumPointer* out, + const BIGNUM* p, + const BIGNUM* g, + const BIGNUM* priv) { + if (out == nullptr || p == nullptr || g == nullptr || priv == nullptr) { + return false; + } + auto pub = BignumPointer::New(); + BignumCtxPointer ctx(BN_CTX_new()); + if (!pub || !ctx || + BN_mod_exp_mont_consttime(pub.get(), g, priv, p, ctx.get(), nullptr) != + 1) { + return false; + } + + *out = std::move(pub); + return true; +} + +std::optional CheckDhParams(const BIGNUM* p, + const BIGNUM* g, + const BIGNUM* q, + const BIGNUM* j) { + // TODO(panva): In a semver-major, consider tightening OpenSSL 3 validation + // to report generator and q failures as strictly as legacy DH_check(). + if (p == nullptr || g == nullptr) return std::nullopt; + + const int p_bits = BN_num_bits(p); + if (p_bits > OPENSSL_DH_CHECK_MAX_MODULUS_BITS) return std::nullopt; + + int codes = 0; + if (!BN_is_odd(p)) { + codes |= static_cast(DHPointer::CheckResult::P_NOT_PRIME); + } + if (BN_is_negative(g) || BN_is_zero(g) || BN_is_one(g)) { + codes |= static_cast(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR); + } + if (p_bits < 512) { + codes |= static_cast(DHPointer::CheckResult::MODULUS_TOO_SMALL); + } + if (p_bits > OPENSSL_DH_MAX_MODULUS_BITS) { + codes |= static_cast(DHPointer::CheckResult::MODULUS_TOO_LARGE); + } + + BignumCtxPointer ctx(BN_CTX_new()); + if (!ctx) return std::nullopt; + + auto tmp1 = BignumPointer::New(); + auto tmp2 = BignumPointer::New(); + if (!tmp1 || !tmp2) return std::nullopt; + + if (BN_copy(tmp1.get(), p) == nullptr || BN_sub_word(tmp1.get(), 1) != 1) { + return std::nullopt; + } + if (BN_cmp(g, tmp1.get()) >= 0) { + codes |= static_cast(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR); + } + + bool q_good = false; + if (q != nullptr) { + if (BN_ucmp(p, q) > 0) { + q_good = true; + } else { + codes |= static_cast(DHPointer::CheckResult::INVALID_Q); + } + } + + if (q_good) { + if (BN_cmp(g, BN_value_one()) <= 0 || BN_cmp(g, p) >= 0) { + codes |= static_cast(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR); + } else if (BN_mod_exp(tmp1.get(), g, q, p, ctx.get()) != 1) { + return std::nullopt; + } else if (!BN_is_one(tmp1.get())) { + codes |= static_cast(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR); + } + + const int q_is_prime = BN_check_prime(q, ctx.get(), nullptr); + if (q_is_prime < 0) return std::nullopt; + if (q_is_prime == 0) { + codes |= static_cast(DHPointer::CheckResult::Q_NOT_PRIME); + } + + if (BN_div(tmp1.get(), tmp2.get(), p, q, ctx.get()) != 1) { + return std::nullopt; + } + if (!BN_is_one(tmp2.get())) { + codes |= static_cast(DHPointer::CheckResult::INVALID_Q); + } + if (j != nullptr && BN_cmp(j, tmp1.get()) != 0) { + codes |= static_cast(DHPointer::CheckResult::INVALID_J); + } + } + + const int p_is_prime = BN_check_prime(p, ctx.get(), nullptr); + if (p_is_prime < 0) return std::nullopt; + if (p_is_prime == 0) { + codes |= static_cast(DHPointer::CheckResult::P_NOT_PRIME); + } else if (q == nullptr) { + if (BN_rshift1(tmp1.get(), p) != 1) return std::nullopt; + const int q_is_prime = BN_check_prime(tmp1.get(), ctx.get(), nullptr); + if (q_is_prime < 0) return std::nullopt; + if (q_is_prime == 0) { + codes |= static_cast(DHPointer::CheckResult::P_NOT_SAFE_PRIME); + } + } + + return codes; +} +#endif } // namespace +#if NCRYPTO_USE_OPENSSL3_PROVIDER +DHPointer::DHPointer(EVPKeyPointer&& key, const char* group_name) + : dh_(key.release()), group_name_(group_name) {} + +DHPointer::DHPointer(BignumPointer&& p, + BignumPointer&& g, + const char* group_name) + : p_(std::move(p)), g_(std::move(g)), group_name_(group_name) {} +#else DHPointer::DHPointer(DH* dh) : dh_(dh) {} +#endif -DHPointer::DHPointer(DHPointer&& other) noexcept : dh_(other.release()) {} +DHPointer::DHPointer(DHPointer&& other) noexcept +#if NCRYPTO_USE_OPENSSL3_PROVIDER + : dh_(other.dh_.release()), + p_(std::move(other.p_)), + g_(std::move(other.g_)), + pub_key_(std::move(other.pub_key_)), + pvt_key_(std::move(other.pvt_key_)), + group_name_(other.group_name_) { + other.group_name_ = nullptr; +} +#else + : dh_(other.release()) { +} +#endif DHPointer& DHPointer::operator=(DHPointer&& other) noexcept { if (this == &other) return *this; @@ -1576,13 +1987,45 @@ DHPointer::~DHPointer() { reset(); } -void DHPointer::reset(DH* dh) { +void DHPointer::reset( +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_PKEY* dh +#else + DH* dh +#endif +) { dh_.reset(dh); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + p_.reset(); + g_.reset(); + pub_key_.reset(); + pvt_key_.reset(); + group_name_ = nullptr; +#endif } +#if NCRYPTO_USE_OPENSSL3_PROVIDER +EVP_PKEY* DHPointer::release() { + if (!dh_ && p_ && g_) { + auto pkey = + group_name_ != nullptr + ? NewDhPKey(group_name_, pub_key_.get(), pvt_key_.get()) + : NewDhPKey(p_.get(), g_.get(), pub_key_.get(), pvt_key_.get()); + if (!pkey) return nullptr; + dh_.reset(pkey.release()); + } + p_.reset(); + g_.reset(); + pub_key_.reset(); + pvt_key_.reset(); + group_name_ = nullptr; + return dh_.release(); +} +#else DH* DHPointer::release() { return dh_.release(); } +#endif BignumPointer DHPointer::FindGroup(const std::string_view name, FindGroupOption option) { @@ -1608,7 +2051,7 @@ BignumPointer DHPointer::FindGroup(const std::string_view name, BignumPointer DHPointer::GetStandardGenerator() { auto bn = BignumPointer::New(); if (!bn) return {}; - if (!bn.setWord(DH_GENERATOR_2)) return {}; + if (!bn.setWord(2)) return {}; return bn; } @@ -1620,12 +2063,22 @@ DHPointer DHPointer::FromGroup(const std::string_view name, auto generator = GetStandardGenerator(); if (!generator) return {}; // Unable to create the generator. +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const char* group_name = GetOpenSSLDhGroupName(name, option); + return DHPointer(std::move(group), std::move(generator), group_name); +#else return New(std::move(group), std::move(generator)); +#endif } DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) { if (!p || !g) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + auto pkey = NewDhPKey(p.get(), g.get()); + if (!pkey) return {}; + return DHPointer(std::move(pkey)); +#else DHPointer dh(DH_new()); if (!dh) return {}; @@ -1640,9 +2093,21 @@ DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) { g.release(); return dh; +#endif } DHPointer DHPointer::New(size_t bits, unsigned int generator) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + auto param_ctx = EVPKeyCtxPointer::NewFromID(EVP_PKEY_DH); + if (!param_ctx.initForParamgen() || + !param_ctx.setDhParameters(bits, generator)) { + return {}; + } + + auto key_params = param_ctx.paramgen(); + if (!key_params) return {}; + return DHPointer(std::move(key_params)); +#else DHPointer dh(DH_new()); if (!dh) return {}; @@ -1651,23 +2116,103 @@ DHPointer DHPointer::New(size_t bits, unsigned int generator) { } return dh; +#endif } DHPointer::CheckResult DHPointer::check() { ClearErrorOnReturn clearErrorOnReturn; - if (!dh_) return DHPointer::CheckResult::NONE; + if (!*this) return DHPointer::CheckResult::NONE; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // TODO(panva): In a semver-major, consider validating named DH groups + // through the provider instead of preserving the historical verifyError. + if (group_name_ != nullptr) return CheckResult::NONE; + + DeleteFnPtr p; + DeleteFnPtr g; + DeleteFnPtr q; + DeleteFnPtr j; + const BIGNUM* p_bn = p_.get(); + const BIGNUM* g_bn = g_.get(); + const BIGNUM* q_bn = nullptr; + const BIGNUM* j_bn = nullptr; + if ((p_bn == nullptr || g_bn == nullptr) && + !GetDhParams(dh_.get(), &p, &g, &q, &j)) { + return DHPointer::CheckResult::CHECK_FAILED; + } + if (p_bn == nullptr) p_bn = p.get(); + if (g_bn == nullptr) g_bn = g.get(); + q_bn = q.get(); + j_bn = j.get(); + if (p_bn == nullptr || g_bn == nullptr) { + return DHPointer::CheckResult::CHECK_FAILED; + } + + auto codes = CheckDhParams(p_bn, g_bn, q_bn, j_bn); + if (!codes) return DHPointer::CheckResult::CHECK_FAILED; + return static_cast(*codes); +#else int codes = 0; if (DH_check(dh_.get(), &codes) != 1) return DHPointer::CheckResult::CHECK_FAILED; return static_cast(codes); +#endif } DHPointer::CheckPublicKeyResult DHPointer::checkPublicKey( const BignumPointer& pub_key) { ClearErrorOnReturn clearErrorOnReturn; - if (!pub_key || !dh_) { + if (!pub_key || !*this) { + return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + DeleteFnPtr p; + DeleteFnPtr g; + const BIGNUM* p_bn = p_.get(); + const BIGNUM* g_bn = g_.get(); + if ((p_bn == nullptr || g_bn == nullptr) && !GetDhParams(dh_.get(), &p, &g)) { + return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + } + if (p_bn == nullptr) p_bn = p.get(); + if (g_bn == nullptr) g_bn = g.get(); + if (p_bn == nullptr || g_bn == nullptr) { + return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + } + + if (BN_cmp(pub_key.get(), BN_value_one()) <= 0) { + return DHPointer::CheckPublicKeyResult::TOO_SMALL; + } + + DeleteFnPtr p_minus_one(BN_dup(p_bn)); + if (!p_minus_one || BN_sub_word(p_minus_one.get(), 1) != 1) { return DHPointer::CheckPublicKeyResult::CHECK_FAILED; } + + if (BN_cmp(pub_key.get(), p_minus_one.get()) >= 0) { + return DHPointer::CheckPublicKeyResult::TOO_LARGE; + } + + if (p_) { + if (group_name_ == nullptr) return CheckPublicKeyResult::NONE; + + auto peer = NewDhPKey(group_name_, pub_key.get()); + if (!peer) return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(peer.get(), nullptr)); + if (!ctx) return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + if (EVP_PKEY_public_check(ctx.get()) != 1) { + return DHPointer::CheckPublicKeyResult::INVALID; + } + return CheckPublicKeyResult::NONE; + } + + auto peer = NewDhPKey(p_bn, g_bn, pub_key.get()); + if (!peer) return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(peer.get(), nullptr)); + if (!ctx) return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + if (EVP_PKEY_public_check(ctx.get()) != 1) { + return DHPointer::CheckPublicKeyResult::INVALID; + } + return CheckPublicKeyResult::NONE; +#else int codes = 0; if (DH_check_pub_key(dh_.get(), pub_key.get(), &codes) != 1) { return DHPointer::CheckPublicKeyResult::CHECK_FAILED; @@ -1684,65 +2229,242 @@ DHPointer::CheckPublicKeyResult DHPointer::checkPublicKey( return DHPointer::CheckPublicKeyResult::INVALID; } return CheckPublicKeyResult::NONE; +#endif } DataPointer DHPointer::getPrime() const { - if (!dh_) return {}; + if (!*this) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (p_) return p_.encode(); + + DeleteFnPtr p; + DeleteFnPtr g; + if (!GetDhParams(dh_.get(), &p, &g)) return {}; + return BignumPointer::Encode(p.get()); +#else const BIGNUM* p; DH_get0_pqg(dh_.get(), &p, nullptr, nullptr); return BignumPointer::Encode(p); +#endif +} + +size_t DHPointer::getPrimeBits() const { + if (!*this) return 0; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (p_) return BignumPointer::GetBitCount(p_.get()); + + DeleteFnPtr p; + DeleteFnPtr g; + if (!GetDhParams(dh_.get(), &p, &g)) return 0; + return BignumPointer::GetBitCount(p.get()); +#else + const BIGNUM* p; + DH_get0_pqg(dh_.get(), &p, nullptr, nullptr); + return BignumPointer::GetBitCount(p); +#endif } DataPointer DHPointer::getGenerator() const { - if (!dh_) return {}; + if (!*this) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (g_) return g_.encode(); + + DeleteFnPtr p; + DeleteFnPtr g; + if (!GetDhParams(dh_.get(), &p, &g)) return {}; + return BignumPointer::Encode(g.get()); +#else const BIGNUM* g; DH_get0_pqg(dh_.get(), nullptr, nullptr, &g); return BignumPointer::Encode(g); +#endif } DataPointer DHPointer::getPublicKey() const { + if (!*this) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (pub_key_) return pub_key_.encode(); if (!dh_) return {}; + + DeleteFnPtr pub_key; + DeleteFnPtr pvt_key; + if (!GetDhKeys(dh_.get(), &pub_key, &pvt_key)) return {}; + return BignumPointer::Encode(pub_key.get()); +#else const BIGNUM* pub_key; DH_get0_key(dh_.get(), &pub_key, nullptr); return BignumPointer::Encode(pub_key); +#endif } DataPointer DHPointer::getPrivateKey() const { + if (!*this) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (pvt_key_) return pvt_key_.encode(); if (!dh_) return {}; + + DeleteFnPtr pub_key; + DeleteFnPtr pvt_key; + if (!GetDhKeys(dh_.get(), &pub_key, &pvt_key)) return {}; + return BignumPointer::Encode(pvt_key.get()); +#else const BIGNUM* pvt_key; DH_get0_key(dh_.get(), nullptr, &pvt_key); return BignumPointer::Encode(pvt_key); +#endif } bool DHPointer::hasPrivateKey() const { + if (!*this) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (pvt_key_) return true; if (!dh_) return false; + + DeleteFnPtr pub_key; + DeleteFnPtr pvt_key; + if (!GetDhKeys(dh_.get(), &pub_key, &pvt_key)) return false; + return pvt_key != nullptr; +#else const BIGNUM* pvt_key = nullptr; DH_get0_key(dh_.get(), nullptr, &pvt_key); return pvt_key != nullptr; +#endif } -DataPointer DHPointer::generateKeys() const { +DataPointer DHPointer::generateKeys() { ClearErrorOnReturn clearErrorOnReturn; - if (!dh_) return {}; + if (!*this) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (p_ && g_) { + if (!pvt_key_ && !GenerateDhPrivateKey(&pvt_key_, p_.get(), group_name_)) { + return {}; + } + + BignumPointer generated_pub_key; + if (!GenerateDhPublicKey( + &generated_pub_key, p_.get(), g_.get(), pvt_key_.get())) { + return {}; + } + + if (pub_key_ && BN_cmp(pub_key_.get(), generated_pub_key.get()) == 0) { + return getPublicKey(); + } + + pub_key_ = std::move(generated_pub_key); + return getPublicKey(); + } + + DeleteFnPtr p; + DeleteFnPtr g; + DeleteFnPtr pub_key; + DeleteFnPtr pvt_key; + if (!GetDhParams(dh_.get(), &p, &g) || + !GetDhKeys(dh_.get(), &pub_key, &pvt_key)) { + return {}; + } + + if (pvt_key != nullptr) { + BignumPointer generated_pub_key; + if (!GenerateDhPublicKey( + &generated_pub_key, p.get(), g.get(), pvt_key.get())) { + return {}; + } + + if (pub_key != nullptr && + BN_cmp(pub_key.get(), generated_pub_key.get()) == 0) { + return getPublicKey(); + } + + auto replacement = + group_name_ != nullptr + ? NewDhPKey(group_name_, generated_pub_key.get(), pvt_key.get()) + : NewDhPKey( + p.get(), g.get(), generated_pub_key.get(), pvt_key.get()); + if (!replacement) return {}; + dh_.reset(replacement.release()); + return getPublicKey(); + } + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(dh_.get(), nullptr)); + if (!ctx || !ctx.initForKeygen()) return {}; + EVP_PKEY* generated = nullptr; + if (EVP_PKEY_keygen(ctx.get(), &generated) != 1) return {}; + dh_.reset(generated); + return getPublicKey(); +#else // Key generation failed if (!DH_generate_key(dh_.get())) return {}; return getPublicKey(); +#endif } size_t DHPointer::size() const { - if (!dh_) return 0; + if (!*this) return 0; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (p_) return BignumPointer::GetByteCount(p_.get()); + + const int bits = EVP_PKEY_get_bits(dh_.get()); + return bits > 0 ? (static_cast(bits) + 7) / 8 : 0; +#else int ret = DH_size(dh_.get()); // DH_size can return a -1 on error but we just want to return a 0 // in that case so we don't wrap around when returning the size_t. return ret >= 0 ? static_cast(ret) : 0; +#endif } DataPointer DHPointer::computeSecret(const BignumPointer& peer) const { ClearErrorOnReturn clearErrorOnReturn; - if (!dh_ || !peer) return {}; + if (!*this || !peer) return {}; + +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (p_ && pvt_key_) { + auto secret = BignumPointer::NewSecure(); + BignumCtxPointer ctx(BN_CTX_new()); + if (!secret || !ctx || + BN_mod_exp_mont_consttime(secret.get(), + peer.get(), + pvt_key_.get(), + p_.get(), + ctx.get(), + nullptr) != 1) { + return {}; + } + return secret.encodePadded(size()); + } + + EVPKeyPointer peer_key; + if (group_name_ != nullptr) { + peer_key = NewDhPKey(group_name_, peer.get()); + } else { + DeleteFnPtr p; + DeleteFnPtr g; + if (!GetDhParams(dh_.get(), &p, &g)) return {}; + peer_key = NewDhPKey(p.get(), g.get(), peer.get()); + } + if (!peer_key) return {}; + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(dh_.get(), nullptr)); + size_t out_size = size(); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) != 1 || + EVP_PKEY_CTX_set_dh_pad(ctx.get(), 1) != 1 || + EVP_PKEY_derive_set_peer(ctx.get(), peer_key.get()) != 1 || + EVP_PKEY_derive(ctx.get(), nullptr, &out_size) != 1) { + return {}; + } + + if (out_size == 0) return {}; + auto dp = DataPointer::Alloc(out_size); + if (!dp) return {}; + if (EVP_PKEY_derive( + ctx.get(), static_cast(dp.get()), &out_size) != 1) { + return {}; + } + return dp.resize(out_size); +#else auto dp = DataPointer::Alloc(size()); if (!dp) return {}; @@ -1760,10 +2482,35 @@ DataPointer DHPointer::computeSecret(const BignumPointer& peer) const { } return dp; +#endif } bool DHPointer::setPublicKey(BignumPointer&& key) { - if (!dh_) return false; + if (!*this) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (p_ && g_) { + pub_key_ = std::move(key); + return true; + } + + DeleteFnPtr pub_key; + DeleteFnPtr pvt_key; + if (!GetDhKeys(dh_.get(), &pub_key, &pvt_key)) { + return false; + } + EVPKeyPointer pkey; + if (group_name_ != nullptr) { + pkey = NewDhPKey(group_name_, key.get(), pvt_key.get()); + } else { + DeleteFnPtr p; + DeleteFnPtr g; + if (!GetDhParams(dh_.get(), &p, &g)) return false; + pkey = NewDhPKey(p.get(), g.get(), key.get(), pvt_key.get()); + } + if (!pkey) return false; + dh_.reset(pkey.release()); + return true; +#else if (DH_set0_key(dh_.get(), key.get(), nullptr) == 1) { // If DH_set0_key returns successfully, then dh_ takes ownership of the // BIGNUM, so we must release it here. Unfortunately coverity does not @@ -1773,10 +2520,35 @@ bool DHPointer::setPublicKey(BignumPointer&& key) { return true; } return false; +#endif } bool DHPointer::setPrivateKey(BignumPointer&& key) { - if (!dh_) return false; + if (!*this) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (p_ && g_) { + pvt_key_ = std::move(key); + return true; + } + + DeleteFnPtr pub_key; + DeleteFnPtr pvt_key; + if (!GetDhKeys(dh_.get(), &pub_key, &pvt_key)) { + return false; + } + EVPKeyPointer pkey; + if (group_name_ != nullptr) { + pkey = NewDhPKey(group_name_, pub_key.get(), key.get()); + } else { + DeleteFnPtr p; + DeleteFnPtr g; + if (!GetDhParams(dh_.get(), &p, &g)) return false; + pkey = NewDhPKey(p.get(), g.get(), pub_key.get(), key.get()); + } + if (!pkey) return false; + dh_.reset(pkey.release()); + return true; +#else if (DH_set0_key(dh_.get(), nullptr, key.get()) == 1) { // If DH_set0_key returns successfully, then dh_ takes ownership of the // BIGNUM, so we must release it here. Unfortunately coverity does not @@ -1786,6 +2558,7 @@ bool DHPointer::setPrivateKey(BignumPointer&& key) { return true; } return false; +#endif } DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, @@ -1794,8 +2567,12 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, if (!ourKey || !theirKey) return {}; auto ctx = EVPKeyCtxPointer::New(ourKey); - if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || - EVP_PKEY_derive_set_peer(ctx.get(), theirKey.get()) <= 0 || + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0) { + return {}; + } + // TODO(panva): In a semver-major, consider padding OpenSSL 3 DH derivation + // results here to match DiffieHellman::computeSecret(). + if (EVP_PKEY_derive_set_peer(ctx.get(), theirKey.get()) <= 0 || EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) { return {}; } @@ -2209,14 +2986,60 @@ EVPKeyPointer EVPKeyPointer::NewRawSeed( EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) { if (!dh) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return EVPKeyPointer(dh.release()); +#else auto key = New(); if (!key) return {}; if (EVP_PKEY_assign_DH(key.get(), dh.get())) { dh.release(); } return key; +#endif } +#if NCRYPTO_USE_OPENSSL3_PROVIDER +EVPKeyPointer EVPKeyPointer::NewRSA(const Rsa& rsa) { + const auto public_key = rsa.getPublicKey(); + if (public_key.n == nullptr || public_key.e == nullptr) return {}; + + OSSLParamBldPointer bld(OSSL_PARAM_BLD_new()); + if (!bld || + OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_RSA_N, public_key.n) != + 1 || + OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_RSA_E, public_key.e) != + 1) { + return {}; + } + + int selection = EVP_PKEY_PUBLIC_KEY; + if (public_key.d != nullptr) { + const auto private_key = rsa.getPrivateKey(); + if (private_key.p == nullptr || private_key.q == nullptr || + private_key.dp == nullptr || private_key.dq == nullptr || + private_key.qi == nullptr || + OSSL_PARAM_BLD_push_BN( + bld.get(), OSSL_PKEY_PARAM_RSA_D, public_key.d) != 1 || + OSSL_PARAM_BLD_push_BN( + bld.get(), OSSL_PKEY_PARAM_RSA_FACTOR1, private_key.p) != 1 || + OSSL_PARAM_BLD_push_BN( + bld.get(), OSSL_PKEY_PARAM_RSA_FACTOR2, private_key.q) != 1 || + OSSL_PARAM_BLD_push_BN( + bld.get(), OSSL_PKEY_PARAM_RSA_EXPONENT1, private_key.dp) != 1 || + OSSL_PARAM_BLD_push_BN( + bld.get(), OSSL_PKEY_PARAM_RSA_EXPONENT2, private_key.dq) != 1 || + OSSL_PARAM_BLD_push_BN( + bld.get(), OSSL_PKEY_PARAM_RSA_COEFFICIENT1, private_key.qi) != 1) { + return {}; + } + selection = EVP_PKEY_KEYPAIR; + } + + OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); + if (!params) return {}; + return NewPKeyFromData(EVP_PKEY_RSA, selection, params.get()); +} +#else EVPKeyPointer EVPKeyPointer::NewRSA(RSAPointer&& rsa) { if (!rsa) return {}; auto key = New(); @@ -2226,6 +3049,7 @@ EVPKeyPointer EVPKeyPointer::NewRSA(RSAPointer&& rsa) { } return key; } +#endif // NCRYPTO_USE_OPENSSL3_PROVIDER EVPKeyPointer::EVPKeyPointer(EVP_PKEY* pkey) : pkey_(pkey) {} @@ -2363,18 +3187,83 @@ BIOPointer EVPKeyPointer::derPublicKey() const { bool EVPKeyPointer::assign(const ECKeyPointer& eckey) { if (!pkey_ || !eckey) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return set(eckey); +#else return EVP_PKEY_assign_EC_KEY(pkey_.get(), eckey.get()); +#endif } bool EVPKeyPointer::set(const ECKeyPointer& eckey) { if (!pkey_ || !eckey) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const int nid = EC_GROUP_get_curve_name(eckey.group_.get()); + const char* group_name = OBJ_nid2sn(nid); + if (group_name == nullptr) return false; + + OSSLParamBldPointer bld(OSSL_PARAM_BLD_new()); + if (!bld || OSSL_PARAM_BLD_push_utf8_string( + bld.get(), OSSL_PKEY_PARAM_GROUP_NAME, group_name, 0) != 1) { + return false; + } + + int selection = EVP_PKEY_KEY_PARAMETERS; + OpenSSLBufferPointer encoded_public_key; + ECPointPointer generated_public_key; + const EC_POINT* public_key = eckey.pub_.get(); + if (public_key == nullptr && eckey.priv_ != nullptr) { + generated_public_key = ECPointPointer::New(eckey.group_.get()); + if (!generated_public_key || + !generated_public_key.mul(eckey.group_.get(), eckey.priv_.get())) { + return false; + } + public_key = generated_public_key.get(); + } + + if (public_key != nullptr) { + unsigned char* encoded_public_key_raw = nullptr; + const size_t encoded_public_key_len = + EC_POINT_point2buf(eckey.group_.get(), + public_key, + POINT_CONVERSION_UNCOMPRESSED, + &encoded_public_key_raw, + nullptr); + if (encoded_public_key_len == 0) return false; + encoded_public_key.reset(encoded_public_key_raw); + if (OSSL_PARAM_BLD_push_octet_string(bld.get(), + OSSL_PKEY_PARAM_PUB_KEY, + encoded_public_key.get(), + encoded_public_key_len) != 1) { + return false; + } + selection |= EVP_PKEY_PUBLIC_KEY; + } + + if (eckey.priv_ != nullptr) { + if (OSSL_PARAM_BLD_push_BN( + bld.get(), OSSL_PKEY_PARAM_PRIV_KEY, eckey.priv_.get()) != 1) { + return false; + } + selection |= EVP_PKEY_PRIVATE_KEY; + } + + OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); + if (!params) return false; + auto pkey = NewPKeyFromData(EVP_PKEY_EC, selection, params.get()); + if (!pkey) return false; + reset(pkey.release()); + return true; +#else return EVP_PKEY_set1_EC_KEY(pkey_.get(), eckey); +#endif } +#if NCRYPTO_USE_LEGACY_KEY_TYPES EVPKeyPointer::operator const EC_KEY*() const { if (!pkey_) return nullptr; return EVP_PKEY_get0_EC_KEY(pkey_.get()); } +#endif // NCRYPTO_USE_LEGACY_KEY_TYPES namespace { @@ -2429,6 +3318,34 @@ constexpr bool IsASN1Sequence(const unsigned char* data, return true; } +constexpr bool ReadASN1Element(const unsigned char* data, + size_t size, + unsigned char tag, + size_t* header_size, + size_t* content_size, + size_t* total_size) { + if (size < 2 || data[0] != tag) return false; + + size_t offset; + size_t length; + if (data[1] & 0x80) { + size_t n_bytes = data[1] & ~0x80; + if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) return false; + length = 0; + for (size_t i = 0; i < n_bytes; i++) length = (length << 8) | data[i + 2]; + offset = 2 + n_bytes; + } else { + offset = 2; + length = data[1]; + } + + if (offset > size || length > size - offset) return false; + *header_size = offset; + *content_size = length; + *total_size = offset + length; + return true; +} + constexpr bool IsEncryptedPrivateKeyInfo( const Buffer& buffer) { // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE. @@ -2546,6 +3463,103 @@ Buffer GetPassphrase( } return pass; } + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +using OSSLEncoderCtxPointer = + DeleteFnPtr; + +bool WriteEncodedPKey(BIO* bio, + const EVP_PKEY* pkey, + int selection, + EVPKeyPointer::PKFormatType format, + const char* structure, + const EVP_CIPHER* cipher = nullptr, + Buffer passphrase = {}) { + const char* output_type = + format == EVPKeyPointer::PKFormatType::PEM ? "PEM" : "DER"; + OSSLEncoderCtxPointer ctx(OSSL_ENCODER_CTX_new_for_pkey( + pkey, selection, output_type, structure, nullptr)); + if (!ctx || OSSL_ENCODER_CTX_get_num_encoders(ctx.get()) == 0) { + return false; + } + + if (cipher != nullptr) { + if (OSSL_ENCODER_CTX_set_cipher( + ctx.get(), EVP_CIPHER_get0_name(cipher), nullptr) != 1 || + OSSL_ENCODER_CTX_set_passphrase( + ctx.get(), + reinterpret_cast(passphrase.data), + passphrase.len) != 1) { + return false; + } + } + + return OSSL_ENCODER_to_bio(ctx.get(), bio) == 1; +} + +struct DERView { + const unsigned char* data = nullptr; + size_t len = 0; +}; + +int WriteDERView(const void* x, unsigned char** out) { + const auto* der = static_cast(x); + if (der == nullptr || der->data == nullptr || + der->len > static_cast(INT_MAX)) { + return -1; + } + if (out != nullptr) { + memcpy(*out, der->data, der->len); + *out += der->len; + } + return static_cast(der->len); +} + +bool WriteEncryptedTraditionalPEM(BIO* bio, + const EVP_PKEY* pkey, + const EVP_CIPHER* cipher, + Buffer passphrase) { + if (passphrase.len > static_cast(INT_MAX)) return false; + + unsigned char* der = nullptr; + size_t der_len = 0; + OSSLEncoderCtxPointer ctx(OSSL_ENCODER_CTX_new_for_pkey( + pkey, OSSL_KEYMGMT_SELECT_KEYPAIR, "DER", "pkcs1", nullptr)); + if (!ctx || OSSL_ENCODER_to_data(ctx.get(), &der, &der_len) != 1) { + return false; + } + + OpenSSLBufferPointer der_storage(der); + DERView der_view{der_storage.get(), der_len}; + return PEM_ASN1_write_bio( + WriteDERView, + PEM_STRING_RSA, + bio, + &der_view, + cipher, + reinterpret_cast(passphrase.data), + static_cast(passphrase.len), + nullptr, + nullptr) == 1; +} + +bool ECKeyHasMissingOid(const EVPKeyPointer& key) { + if (key.id() != EVP_PKEY_EC) return false; + + const Ec ec(key.get()); + const EC_GROUP* group = ec.getGroup(); + if (group == nullptr || + EC_GROUP_get_asn1_flag(group) != OPENSSL_EC_NAMED_CURVE) { + return false; + } + + const int nid = EC_GROUP_get_curve_name(group); + if (nid == NID_undef) return true; + + const ASN1_OBJECT* asn1 = OBJ_nid2obj(nid); + return asn1 == nullptr || OBJ_length(asn1) == 0; +} +#endif } // namespace EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( @@ -2633,6 +3647,22 @@ Result EVPKeyPointer::writePrivateKey( // PKCS1 is only permitted for RSA keys. if (id() != EVP_PKEY_RSA) return Result(false); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const EVP_CIPHER* cipher = + config.format == PKFormatType::PEM ? config.cipher : nullptr; + if (cipher != nullptr && passphrase.len == 0) { + err = + !WriteEncryptedTraditionalPEM(bio.get(), get(), cipher, passphrase); + } else { + err = !WriteEncodedPKey(bio.get(), + get(), + OSSL_KEYMGMT_SELECT_ALL, + config.format, + "pkcs1", + cipher, + passphrase); + } +#else #if OPENSSL_VERSION_MAJOR >= 3 const RSA* rsa = EVP_PKEY_get0_RSA(get()); #else @@ -2660,6 +3690,7 @@ Result EVPKeyPointer::writePrivateKey( return Result(false); } } +#endif break; } case PKEncodingType::PKCS8: { @@ -2696,6 +3727,17 @@ Result EVPKeyPointer::writePrivateKey( // SEC1 is only permitted for EC keys if (id() != EVP_PKEY_EC) return Result(false); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const EVP_CIPHER* cipher = + config.format == PKFormatType::PEM ? config.cipher : nullptr; + err = !WriteEncodedPKey(bio.get(), + get(), + OSSL_KEYMGMT_SELECT_ALL, + config.format, + "type-specific", + cipher, + passphrase); +#else #if OPENSSL_VERSION_MAJOR >= 3 const EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get()); #else @@ -2723,6 +3765,7 @@ Result EVPKeyPointer::writePrivateKey( return Result(false); } } +#endif break; } default: { @@ -2749,6 +3792,18 @@ Result EVPKeyPointer::writePublicKey( if (config.type == ncrypto::EVPKeyPointer::PKEncodingType::PKCS1) { // PKCS#1 is only valid for RSA keys. +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (id() != EVP_PKEY_RSA) return Result(false); + if (!WriteEncodedPKey(bio.get(), + get(), + OSSL_KEYMGMT_SELECT_PUBLIC_KEY, + config.format, + "pkcs1")) { + return Result(false, + mark_pop_error_on_return.peekError()); + } + return bio; +#else #if OPENSSL_VERSION_MAJOR >= 3 const RSA* rsa = EVP_PKEY_get0_RSA(get()); #else @@ -2769,7 +3824,16 @@ Result EVPKeyPointer::writePublicKey( mark_pop_error_on_return.peekError()); } return bio; +#endif + } + +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (ECKeyHasMissingOid(*this)) { + ERR_raise(ERR_LIB_EC, EC_R_MISSING_OID); + return Result(false, + mark_pop_error_on_return.peekError()); } +#endif if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) { // Encode SPKI as PEM. @@ -2841,11 +3905,23 @@ std::optional EVPKeyPointer::getBytesOfRS() const { int bits, id = base_id(); if (id == EVP_PKEY_DSA) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + DeleteFnPtr q; + if (!GetPKeyBnParam(get(), OSSL_PKEY_PARAM_FFC_Q, &q)) return std::nullopt; + bits = BignumPointer::GetBitCount(q.get()); +#else const DSA* dsa_key = EVP_PKEY_get0_DSA(get()); // Both r and s are computed mod q, so their width is limited by that of q. bits = BignumPointer::GetBitCount(DSA_get0_q(dsa_key)); +#endif } else if (id == EVP_PKEY_EC) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + Ec ec(get()); + if (!ec) return std::nullopt; + bits = EC_GROUP_order_bits(ec.getGroup()); +#else bits = EC_GROUP_order_bits(ECKeyPointer::GetGroup(*this)); +#endif } else { return std::nullopt; } @@ -2857,6 +3933,9 @@ EVPKeyPointer::operator Rsa() const { int type = id(); if (type != EVP_PKEY_RSA && type != EVP_PKEY_RSA_PSS) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return Rsa(get()); +#else // TODO(tniessen): Remove the "else" branch once we drop support for OpenSSL // versions older than 1.1.1e via FIPS / dynamic linking. OSSL3_CONST RSA* rsa; @@ -2867,15 +3946,20 @@ EVPKeyPointer::operator Rsa() const { } if (rsa == nullptr) return {}; return Rsa(rsa); +#endif } EVPKeyPointer::operator Dsa() const { int type = id(); if (type != EVP_PKEY_DSA) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return Dsa(get()); +#else OSSL3_CONST DSA* dsa = EVP_PKEY_get0_DSA(get()); if (dsa == nullptr) return {}; return Dsa(dsa); +#endif } bool EVPKeyPointer::validateDsaParameters() const { @@ -2886,12 +3970,25 @@ bool EVPKeyPointer::validateDsaParameters() const { #else if (FIPS_mode() && EVP_PKEY_DSA == id()) { #endif +#if NCRYPTO_USE_OPENSSL3_PROVIDER + DeleteFnPtr p; + DeleteFnPtr q; + if (!GetPKeyBnParam(pkey_.get(), OSSL_PKEY_PARAM_FFC_P, &p) || + !GetPKeyBnParam(pkey_.get(), OSSL_PKEY_PARAM_FFC_Q, &q)) { + return false; + } + const BIGNUM* p_value = p.get(); + const BIGNUM* q_value = q.get(); +#else const DSA* dsa = EVP_PKEY_get0_DSA(pkey_.get()); const BIGNUM* p; const BIGNUM* q; DSA_get0_pqg(dsa, &p, &q, nullptr); - int L = BignumPointer::GetBitCount(p); - int N = BignumPointer::GetBitCount(q); + const BIGNUM* p_value = p; + const BIGNUM* q_value = q; +#endif + int L = BignumPointer::GetBitCount(p_value); + int N = BignumPointer::GetBitCount(q_value); return (L == 1024 && N == 160) || (L == 2048 && N == 224) || (L == 2048 && N == 256) || (L == 3072 && N == 256); @@ -3196,7 +4293,7 @@ const Cipher Cipher::FromNid(int nid) { } const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) { - return Cipher(EVP_CIPHER_CTX_cipher(ctx.get())); + return Cipher(GetCipherCtxCipher(ctx.get())); } const Cipher Cipher::EMPTY = Cipher(); @@ -3345,7 +4442,7 @@ int Cipher::bytesToKey(const Digest& digest, CipherCtxPointer CipherCtxPointer::New() { auto ret = CipherCtxPointer(EVP_CIPHER_CTX_new()); if (!ret) return {}; - EVP_CIPHER_CTX_init(ret.get()); + EVP_CIPHER_CTX_reset(ret.get()); return ret; } @@ -3609,8 +4706,15 @@ bool ECPointPointer::mul(const EC_GROUP* group, const BIGNUM* priv_key) { // ============================================================================ +#if NCRYPTO_USE_LEGACY_KEY_TYPES ECKeyPointer::ECKeyPointer() : key_(nullptr) {} +ECKeyPointer::ECKeyPointer(const EVPKeyPointer& key) : key_(nullptr) { + if (key.id() != EVP_PKEY_EC) return; + const EC_KEY* ec = key; + if (ec != nullptr) key_.reset(EC_KEY_dup(ec)); +} + ECKeyPointer::ECKeyPointer(EC_KEY* key) : key_(key) {} ECKeyPointer::ECKeyPointer(ECKeyPointer&& other) noexcept @@ -3731,6 +4835,20 @@ bool ECKeyPointer::checkKey() const { return Check(key_.get()); } +DataPointer ECKeyPointer::computeSecret(const ECPointPointer& peer) const { + if (!key_ || !peer) return {}; + const EC_GROUP* group = getGroup(); + const int field_size = EC_GROUP_get_degree(group); + const size_t out_len = (field_size + 7) / 8; + auto out = DataPointer::Alloc(out_len); + if (!out) return {}; + if (ECDH_compute_key( + out.get(), out.size(), peer.get(), key_.get(), nullptr) == 0) { + return {}; + } + return out; +} + ECKeyPointer ECKeyPointer::NewByCurveName(int nid) { return ECKeyPointer(EC_KEY_new_by_curve_name(nid)); } @@ -3741,6 +4859,262 @@ ECKeyPointer ECKeyPointer::New(const EC_GROUP* group) { if (!EC_KEY_set_group(ptr.get(), group)) return {}; return ptr; } +#else +ECKeyPointer::ECKeyPointer() : group_(nullptr), pub_(nullptr), priv_(nullptr) {} + +ECKeyPointer::ECKeyPointer(const EVPKeyPointer& key) : ECKeyPointer() { + if (key.id() != EVP_PKEY_EC) return; + char group_name[80]; + size_t group_name_len = 0; + if (EVP_PKEY_get_utf8_string_param(key.get(), + OSSL_PKEY_PARAM_GROUP_NAME, + group_name, + sizeof(group_name), + &group_name_len) != 1) { + return; + } + + const int nid = Ec::GetCurveIdFromName(group_name); + if (nid == NID_undef) return; + group_.reset(EC_GROUP_new_by_curve_name(nid)); + if (!group_) return; + + GetOptionalPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, &priv_); + + size_t public_key_len = 0; + if (EVP_PKEY_get_octet_string_param( + key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &public_key_len) == + 1) { + auto public_key = DataPointer::Alloc(public_key_len); + if (!public_key || EVP_PKEY_get_octet_string_param( + key.get(), + OSSL_PKEY_PARAM_PUB_KEY, + static_cast(public_key.get()), + public_key.size(), + &public_key_len) != 1) { + reset(); + return; + } + + auto point = ECPointPointer::New(group_.get()); + if (!point || + !point.setFromBuffer( + {static_cast(public_key.get()), public_key_len}, + group_.get())) { + reset(); + return; + } + pub_.reset(point.release()); + } +} + +ECKeyPointer::ECKeyPointer(ECKeyPointer&& other) noexcept + : group_(std::move(other.group_)), + pub_(std::move(other.pub_)), + priv_(std::move(other.priv_)) {} + +ECKeyPointer& ECKeyPointer::operator=(ECKeyPointer&& other) noexcept { + group_ = std::move(other.group_); + pub_ = std::move(other.pub_); + priv_ = std::move(other.priv_); + return *this; +} + +ECKeyPointer::~ECKeyPointer() { + reset(); +} + +void ECKeyPointer::reset() { + group_.reset(); + pub_.reset(); + priv_.reset(); +} + +ECKeyPointer ECKeyPointer::clone() const { + if (!group_) return {}; + ECKeyPointer ret; + ret.group_.reset(EC_GROUP_dup(group_.get())); + if (!ret.group_) return {}; + if (pub_ != nullptr) { + ret.pub_.reset(EC_POINT_dup(pub_.get(), ret.group_.get())); + if (!ret.pub_) return {}; + } + if (priv_ != nullptr) { + ret.priv_.reset(BN_dup(priv_.get())); + if (!ret.priv_) return {}; + } + return ret; +} + +bool ECKeyPointer::generate() { + if (!group_) return false; + const int nid = EC_GROUP_get_curve_name(group_.get()); + auto ctx = EVPKeyCtxPointer::NewFromID(EVP_PKEY_EC); + if (!ctx || !ctx.initForKeygen() || + !ctx.setEcParameters(nid, OPENSSL_EC_NAMED_CURVE)) { + return false; + } + + EVP_PKEY* raw = nullptr; + if (EVP_PKEY_keygen(ctx.get(), &raw) != 1) return false; + EVPKeyPointer pkey(raw); + + DeleteFnPtr priv; + if (!GetPKeyBnParam(pkey.get(), OSSL_PKEY_PARAM_PRIV_KEY, &priv)) { + return false; + } + + size_t public_key_len = 0; + if (EVP_PKEY_get_octet_string_param( + pkey.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &public_key_len) != + 1) { + return false; + } + + auto public_key = DataPointer::Alloc(public_key_len); + if (!public_key || EVP_PKEY_get_octet_string_param( + pkey.get(), + OSSL_PKEY_PARAM_PUB_KEY, + static_cast(public_key.get()), + public_key.size(), + &public_key_len) != 1) { + return false; + } + + auto point = ECPointPointer::New(group_.get()); + if (!point || + !point.setFromBuffer( + {static_cast(public_key.get()), public_key_len}, + group_.get())) { + return false; + } + + priv_ = std::move(priv); + pub_.reset(point.release()); + return true; +} + +bool ECKeyPointer::setPublicKey(const ECPointPointer& pub) { + if (!group_ || !pub) return false; + pub_.reset(EC_POINT_dup(pub.get(), group_.get())); + return pub_ != nullptr; +} + +bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x, + const BignumPointer& y) { + if (!group_ || !x || !y) return false; + const size_t field_len = (EC_GROUP_get_degree(group_.get()) + 7) / 8; + const size_t uncompressed_len = 1 + 2 * field_len; + auto buf = DataPointer::Alloc(uncompressed_len); + if (!buf) return false; + unsigned char* ptr = static_cast(buf.get()); + ptr[0] = POINT_CONVERSION_UNCOMPRESSED; + x.encodePaddedInto(ptr + 1, field_len); + y.encodePaddedInto(ptr + 1 + field_len, field_len); + + auto point = ECPointPointer::New(group_.get()); + if (!point || !point.setFromBuffer({ptr, uncompressed_len}, group_.get())) { + return false; + } + pub_.reset(point.release()); + return true; +} + +bool ECKeyPointer::setPrivateKey(const BignumPointer& priv) { + if (!group_ || !priv) return false; + priv_.reset(BN_dup(priv.get())); + return priv_ != nullptr; +} + +const BIGNUM* ECKeyPointer::getPrivateKey() const { + return priv_.get(); +} + +const EC_POINT* ECKeyPointer::getPublicKey() const { + return pub_.get(); +} + +const EC_GROUP* ECKeyPointer::getGroup() const { + return group_.get(); +} + +bool ECKeyPointer::checkKey() const { + if (!group_) return false; + + if (priv_ != nullptr) { + auto order = BignumPointer::New(); + if (!order || !EC_GROUP_get_order(group_.get(), order.get(), nullptr)) { + return false; + } + if (BN_is_zero(priv_.get()) || BN_is_negative(priv_.get()) || + BN_cmp(priv_.get(), order.get()) >= 0) { + return false; + } + } + + if (pub_ != nullptr && + EC_POINT_is_on_curve(group_.get(), pub_.get(), nullptr) != 1) { + return false; + } + + if (priv_ != nullptr && pub_ != nullptr) { + auto expected = ECPointPointer::New(group_.get()); + if (!expected || !expected.mul(group_.get(), priv_.get()) || + EC_POINT_cmp(group_.get(), expected.get(), pub_.get(), nullptr) != 0) { + return false; + } + } + + auto pkey = EVPKeyPointer::New(); + if (!pkey || !pkey.set(*this)) return false; + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + if (!ctx) return false; + if (pub_ != nullptr && EVP_PKEY_public_check(ctx.get()) != 1) return false; + if (priv_ != nullptr && EVP_PKEY_private_check(ctx.get()) != 1) return false; + return true; +} + +DataPointer ECKeyPointer::computeSecret(const ECPointPointer& peer) const { + if (!group_ || !priv_ || !peer) return {}; + auto our_key = EVPKeyPointer::New(); + auto their_key = EVPKeyPointer::New(); + auto their_ec = ECKeyPointer::New(group_.get()); + if (!our_key || !their_key || !our_key.set(*this) || + !their_ec.setPublicKey(peer) || !their_key.set(their_ec)) { + return {}; + } + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr)); + size_t out_len = 0; + if (!ctx || EVP_PKEY_derive_init(ctx.get()) != 1 || + EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) != 1 || + EVP_PKEY_derive(ctx.get(), nullptr, &out_len) != 1) { + return {}; + } + + auto out = DataPointer::Alloc(out_len); + if (!out) return {}; + if (EVP_PKEY_derive( + ctx.get(), static_cast(out.get()), &out_len) != 1) { + return {}; + } + return out.resize(out_len); +} + +ECKeyPointer ECKeyPointer::NewByCurveName(int nid) { + ECKeyPointer ret; + ret.group_.reset(EC_GROUP_new_by_curve_name(nid)); + return ret; +} + +ECKeyPointer ECKeyPointer::New(const EC_GROUP* group) { + ECKeyPointer ret; + if (group != nullptr) { + ret.group_.reset(EC_GROUP_dup(group)); + } + return ret; +} +#endif // NCRYPTO_USE_LEGACY_KEY_TYPES // ============================================================================ @@ -3835,8 +5209,33 @@ bool EVPKeyCtxPointer::setDsaParameters(uint32_t bits, bool EVPKeyCtxPointer::setEcParameters(int curve, int encoding) { if (!ctx_) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const char* group_name = OBJ_nid2sn(curve); + if (group_name == nullptr) return false; + + const char* encoding_name = nullptr; + switch (encoding) { + case OPENSSL_EC_EXPLICIT_CURVE: + encoding_name = OSSL_PKEY_EC_ENCODING_EXPLICIT; + break; + case OPENSSL_EC_NAMED_CURVE: + encoding_name = OSSL_PKEY_EC_ENCODING_GROUP; + break; + default: + return false; + } + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string( + OSSL_PKEY_PARAM_GROUP_NAME, const_cast(group_name), 0), + OSSL_PARAM_construct_utf8_string( + OSSL_PKEY_PARAM_EC_ENCODING, const_cast(encoding_name), 0), + OSSL_PARAM_END, + }; + return EVP_PKEY_CTX_set_params(ctx_.get(), params) == 1; +#else return EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx_.get(), curve) == 1 && EVP_PKEY_CTX_set_ec_param_enc(ctx_.get(), encoding) == 1; +#endif } bool EVPKeyCtxPointer::setRsaOaepMd(const Digest& md) { @@ -3875,12 +5274,16 @@ bool EVPKeyCtxPointer::setRsaKeygenBits(int bits) { bool EVPKeyCtxPointer::setRsaKeygenPubExp(BignumPointer&& e) { if (!ctx_) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return EVP_PKEY_CTX_set1_rsa_keygen_pubexp(ctx_.get(), e.get()) == 1; +#else if (EVP_PKEY_CTX_set_rsa_keygen_pubexp(ctx_.get(), e.get()) == 1) { // The ctx_ takes ownership of e on success. e.release(); return true; } return false; +#endif } bool EVPKeyCtxPointer::setRsaPssKeygenMd(const Digest& md) { @@ -3934,7 +5337,7 @@ bool EVPKeyCtxPointer::setRsaOaepLabel(DataPointer&& data) { bool EVPKeyCtxPointer::setSignatureMd(const EVPMDCtxPointer& md) { if (!ctx_) return false; - return EVP_PKEY_CTX_set_signature_md(ctx_.get(), EVP_MD_CTX_md(md.get())) == + return EVP_PKEY_CTX_set_signature_md(ctx_.get(), GetDigestCtxMd(md.get())) == 1; } @@ -4124,26 +5527,213 @@ DataPointer CipherImpl(const EVPKeyPointer& key, } } // namespace -Rsa::Rsa() : rsa_(nullptr) {} +#if NCRYPTO_USE_OPENSSL3_PROVIDER +namespace { +int DigestAlgorithmIdentifierToNid(const unsigned char* data, size_t size) { + size_t sequence_header; + size_t sequence_len; + size_t sequence_total; + if (!ReadASN1Element( + data, size, 0x30, &sequence_header, &sequence_len, &sequence_total)) { + return NID_undef; + } + + size_t oid_header; + size_t oid_len; + size_t oid_total; + const unsigned char* oid = data + sequence_header; + if (!ReadASN1Element( + oid, sequence_len, 0x06, &oid_header, &oid_len, &oid_total)) { + return NID_undef; + } + + const unsigned char* oid_data = oid; + DeleteFnPtr obj( + d2i_ASN1_OBJECT(nullptr, &oid_data, oid_total)); + if (!obj) return NID_undef; + return OBJ_obj2nid(obj.get()); +} + +bool ReadRsaPssParams(const EVP_PKEY* pkey, Rsa::PssParams* params) { + const int der_len = i2d_PUBKEY(pkey, nullptr); + if (der_len <= 0) return false; + + auto der = DataPointer::Alloc(der_len); + if (!der) return false; + + auto serialized = static_cast(der.get()); + if (i2d_PUBKEY(pkey, &serialized) != der_len) return false; + + size_t outer_header; + size_t outer_len; + size_t outer_total; + const auto* data = static_cast(der.get()); + if (!ReadASN1Element( + data, der.size(), 0x30, &outer_header, &outer_len, &outer_total)) { + return false; + } + + size_t alg_header; + size_t alg_len; + size_t alg_total; + const unsigned char* alg = data + outer_header; + if (!ReadASN1Element( + alg, outer_len, 0x30, &alg_header, &alg_len, &alg_total)) { + return false; + } + + size_t oid_header; + size_t oid_len; + size_t oid_total; + const unsigned char* oid = alg + alg_header; + if (!ReadASN1Element(oid, alg_len, 0x06, &oid_header, &oid_len, &oid_total) || + oid_total == alg_len) { + return false; + } + + size_t pss_header; + size_t pss_len; + size_t pss_total; + const unsigned char* pss = oid + oid_total; + if (!ReadASN1Element( + pss, alg_len - oid_total, 0x30, &pss_header, &pss_len, &pss_total)) { + return false; + } + + const unsigned char* cursor = pss + pss_header; + size_t remaining = pss_len; + while (remaining > 0) { + const unsigned char tag = cursor[0]; + size_t item_header; + size_t item_len; + size_t item_total; + if (!ReadASN1Element( + cursor, remaining, tag, &item_header, &item_len, &item_total)) { + return false; + } + + const unsigned char* item = cursor + item_header; + switch (tag) { + case 0xa0: { + const int nid = DigestAlgorithmIdentifierToNid(item, item_len); + if (nid != NID_undef) params->digest = OBJ_nid2ln(nid); + break; + } + case 0xa1: { + size_t mgf_header; + size_t mgf_len; + size_t mgf_total; + if (!ReadASN1Element( + item, item_len, 0x30, &mgf_header, &mgf_len, &mgf_total)) { + return false; + } + const unsigned char* mgf = item + mgf_header; + size_t mgf_oid_header; + size_t mgf_oid_len; + size_t mgf_oid_total; + if (!ReadASN1Element(mgf, + mgf_len, + 0x06, + &mgf_oid_header, + &mgf_oid_len, + &mgf_oid_total) || + mgf_oid_total == mgf_len) { + return false; + } + const int nid = DigestAlgorithmIdentifierToNid(mgf + mgf_oid_total, + mgf_len - mgf_oid_total); + if (nid != NID_undef) params->mgf1_digest = OBJ_nid2ln(nid); + break; + } + case 0xa2: { + size_t int_header; + size_t int_len; + size_t int_total; + if (!ReadASN1Element( + item, item_len, 0x02, &int_header, &int_len, &int_total)) { + return false; + } + // TODO(panva): In a semver-major, reject malformed RSA-PSS parameters + // at key import instead of omitting asymmetricKeyDetails fields. + if (int_len == 0 || int_len > sizeof(uint64_t) || + (item[int_header] & 0x80) != 0) { + return false; + } + uint64_t salt_length = 0; + for (size_t n = 0; n < int_len; n++) { + salt_length = (salt_length << 8) | item[int_header + n]; + } + params->salt_length = static_cast(salt_length); + break; + } + } + + cursor += item_total; + remaining -= item_total; + } + + return true; +} +} // namespace + +Rsa::Rsa() : rsa_(false) {} + +Rsa::Rsa(const EVP_PKEY* pkey) : Rsa() { + const int type = EVPKeyPointer::id(pkey); + if (type != EVP_PKEY_RSA && type != EVP_PKEY_RSA_PSS) return; + if (!GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_N, &n_) || + !GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_E, &e_)) { + return; + } + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_D, &d_); + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_FACTOR1, &p_); + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_FACTOR2, &q_); + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_EXPONENT1, &dp_); + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_EXPONENT2, &dq_); + GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_COEFFICIENT1, &qi_); + + if (type == EVP_PKEY_RSA_PSS) { + MarkPopErrorOnReturn pop_errors; + PssParams params; + if (ReadRsaPssParams(pkey, ¶ms)) pss_params_ = params; + } + rsa_ = true; +} +#else +Rsa::Rsa() : rsa_(nullptr) {} Rsa::Rsa(OSSL3_CONST RSA* ptr) : rsa_(ptr) {} +#endif const Rsa::PublicKey Rsa::getPublicKey() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!rsa_) return {}; + return PublicKey{n_.get(), e_.get(), d_.get()}; +#else if (rsa_ == nullptr) return {}; PublicKey key; RSA_get0_key(rsa_, &key.n, &key.e, &key.d); return key; +#endif } const Rsa::PrivateKey Rsa::getPrivateKey() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!rsa_) return {}; + return PrivateKey{p_.get(), q_.get(), dp_.get(), dq_.get(), qi_.get()}; +#else if (rsa_ == nullptr) return {}; PrivateKey key; RSA_get0_factors(rsa_, &key.p, &key.q); RSA_get0_crt_params(rsa_, &key.dp, &key.dq, &key.qi); return key; +#endif } const std::optional Rsa::getPssParams() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return pss_params_; +#else if (rsa_ == nullptr) return std::nullopt; const RSA_PSS_PARAMS* params = RSA_get0_pss_params(rsa_); if (params == nullptr) return std::nullopt; @@ -4176,16 +5766,36 @@ const std::optional Rsa::getPssParams() const { } } return ret; +#endif +} + +BIOPointer Rsa::derPublicKey() const { + auto bio = BIOPointer::NewMem(); + if (!bio) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + auto pkey = EVPKeyPointer::NewRSA(*this); + if (!pkey || i2d_PUBKEY_bio(bio.get(), pkey.get()) != 1) return {}; +#else + if (rsa_ == nullptr || i2d_RSA_PUBKEY_bio(bio.get(), rsa_) != 1) return {}; +#endif + return bio; } bool Rsa::setPublicKey(BignumPointer&& n, BignumPointer&& e) { if (!n || !e) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + n_.reset(n.release()); + e_.reset(e.release()); + rsa_ = true; + return true; +#else if (RSA_set0_key(const_cast(rsa_), n.get(), e.get(), nullptr) == 1) { n.release(); e.release(); return true; } return false; +#endif } bool Rsa::setPrivateKey(BignumPointer&& d, @@ -4194,6 +5804,17 @@ bool Rsa::setPrivateKey(BignumPointer&& d, BignumPointer&& dp, BignumPointer&& dq, BignumPointer&& qi) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!d || !q || !p || !dp || !dq || !qi) return false; + d_.reset(d.release()); + q_.reset(q.release()); + p_.reset(p.release()); + dp_.reset(dp.release()); + dq_.reset(dq.release()); + qi_.reset(qi.release()); + rsa_ = n_ != nullptr && e_ != nullptr; + return rsa_; +#else if (!RSA_set0_key(const_cast(rsa_), nullptr, nullptr, d.get())) { return false; } @@ -4213,6 +5834,7 @@ bool Rsa::setPrivateKey(BignumPointer&& d, dq.release(); qi.release(); return true; +#endif } DataPointer Rsa::encrypt(const EVPKeyPointer& key, @@ -4335,12 +5957,99 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) { // ============================================================================ -Ec::Ec() : ec_(nullptr) {} +#if NCRYPTO_USE_OPENSSL3_PROVIDER +Ec::Ec() : ec_(nullptr), pub_(nullptr) {} + +Ec::Ec(const EVP_PKEY* pkey) : Ec() { + if (EVPKeyPointer::id(pkey) != EVP_PKEY_EC) return; + char group_name[80]; + size_t group_name_len = 0; + if (EVP_PKEY_get_utf8_string_param(pkey, + OSSL_PKEY_PARAM_GROUP_NAME, + group_name, + sizeof(group_name), + &group_name_len) != 1) { + return; + } + + const int nid = GetCurveIdFromName(group_name); + if (nid == NID_undef) return; + ec_.reset(EC_GROUP_new_by_curve_name(nid)); + if (!ec_) return; + + size_t public_key_len = 0; + if (EVP_PKEY_get_octet_string_param( + pkey, OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &public_key_len) != 1) { + return; + } + + auto public_key = DataPointer::Alloc(public_key_len); + if (!public_key || + EVP_PKEY_get_octet_string_param( + pkey, + OSSL_PKEY_PARAM_PUB_KEY, + static_cast(public_key.get()), + public_key.size(), + &public_key_len) != 1 || + public_key_len == 0) { + ec_.reset(); + return; + } + + const auto* public_key_data = + static_cast(public_key.get()); + switch (public_key_data[0]) { + case POINT_CONVERSION_COMPRESSED: + case POINT_CONVERSION_COMPRESSED + 1: + form_ = POINT_CONVERSION_COMPRESSED; + break; + case POINT_CONVERSION_UNCOMPRESSED: + form_ = POINT_CONVERSION_UNCOMPRESSED; + break; + case POINT_CONVERSION_HYBRID: + case POINT_CONVERSION_HYBRID + 1: + form_ = POINT_CONVERSION_HYBRID; + break; + default: + ec_.reset(); + return; + } + auto point = ECPointPointer::New(ec_.get()); + if (!point || + !point.setFromBuffer({public_key_data, public_key_len}, ec_.get())) { + ec_.reset(); + return; + } + pub_.reset(point.release()); +} +#else +Ec::Ec() : ec_(nullptr) {} Ec::Ec(OSSL3_CONST EC_KEY* key) : ec_(key) {} +#endif const EC_GROUP* Ec::getGroup() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return ec_.get(); +#else return ECKeyPointer::GetGroup(ec_); +#endif +} + +const EC_POINT* Ec::getPublicKey() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return pub_.get(); +#else + return ECKeyPointer::GetPublicKey(ec_); +#endif +} + +point_conversion_form_t Ec::getPointConversionForm() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return form_; +#else + return EC_KEY_get_conv_form(ec_); +#endif } int Ec::getCurve() const { @@ -4444,7 +6153,7 @@ size_t EVPMDCtxPointer::getDigestSize() const { const EVP_MD* EVPMDCtxPointer::getDigest() const { if (!ctx_) return nullptr; - return EVP_MD_CTX_md(ctx_.get()); + return GetDigestCtxMd(ctx_.get()); } bool EVPMDCtxPointer::hasXofFlag() const { @@ -4986,31 +6695,61 @@ std::pair X509Name::Iterator::operator*() const { // ============================================================================ -Dsa::Dsa() : dsa_(nullptr) {} +#if NCRYPTO_USE_OPENSSL3_PROVIDER +Dsa::Dsa() : dsa_(false) {} +Dsa::Dsa(const EVP_PKEY* pkey) : Dsa() { + if (EVPKeyPointer::id(pkey) != EVP_PKEY_DSA) return; + if (!GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_P, &p_) || + !GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_Q, &q_)) { + return; + } + dsa_ = true; +} +#else +Dsa::Dsa() : dsa_(nullptr) {} Dsa::Dsa(OSSL3_CONST DSA* dsa) : dsa_(dsa) {} +#endif const BIGNUM* Dsa::getP() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!dsa_) return nullptr; + return p_.get(); +#else if (dsa_ == nullptr) return nullptr; const BIGNUM* p; DSA_get0_pqg(dsa_, &p, nullptr, nullptr); return p; +#endif } const BIGNUM* Dsa::getQ() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!dsa_) return nullptr; + return q_.get(); +#else if (dsa_ == nullptr) return nullptr; const BIGNUM* q; DSA_get0_pqg(dsa_, nullptr, &q, nullptr); return q; +#endif } size_t Dsa::getModulusLength() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!dsa_) return 0; +#else if (dsa_ == nullptr) return 0; +#endif return BignumPointer::GetBitCount(getP()); } size_t Dsa::getDivisorLength() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!dsa_) return 0; +#else if (dsa_ == nullptr) return 0; +#endif return BignumPointer::GetBitCount(getQ()); } diff --git a/deps/ncrypto/ncrypto.gyp b/deps/ncrypto/ncrypto.gyp index 1747f3ea0149b9..804a664fa0a2a1 100644 --- a/deps/ncrypto/ncrypto.gyp +++ b/deps/ncrypto/ncrypto.gyp @@ -2,10 +2,25 @@ 'variables': { 'ncrypto_bssl_libdecrepit_missing%': 1, 'ncrypto_sources': [ - 'engine.cc', 'ncrypto.cc', 'ncrypto.h', ], + 'ncrypto_engine_sources': [ + 'engine.cc', + 'ncrypto.h', + ], + 'ncrypto_strict_defines': [ + 'OPENSSL_API_COMPAT=30000', + 'OPENSSL_NO_DEPRECATED', + ], + 'ncrypto_legacy_openssl_defines': [ + 'OPENSSL_API_COMPAT=0x10100000L', + ], + 'ncrypto_engine_defines': [ + 'OPENSSL_API_COMPAT=30000', + 'OPENSSL_SUPPRESS_DEPRECATED', + 'NCRYPTO_ENGINE_COMPAT=1', + ], }, 'targets': [ { @@ -20,9 +35,25 @@ 'defines': [ 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', ], + 'conditions': [ + ['openssl_is_boringssl=="false" and openssl_version >= 0x3000000f', { + 'defines!': [ '<@(ncrypto_legacy_openssl_defines)' ], + 'defines': [ '<@(ncrypto_strict_defines)' ], + }], + ], }, 'sources': [ '<@(ncrypto_sources)' ], 'conditions': [ + ['openssl_is_boringssl=="false" and openssl_version >= 0x3000000f', { + 'defines!': [ '<@(ncrypto_legacy_openssl_defines)' ], + 'defines': [ '<@(ncrypto_strict_defines)' ], + 'dependencies': [ + 'ncrypto_engine', + ], + }], + ['openssl_is_boringssl=="false" and openssl_version < 0x3000000f', { + 'sources': [ '<@(ncrypto_engine_sources)' ], + }], ['node_shared_openssl=="false"', { 'dependencies': [ '../openssl/openssl.gyp:openssl' @@ -30,5 +61,28 @@ }], ] }, - ] + ], + 'conditions': [ + ['openssl_is_boringssl=="false" and openssl_version >= 0x3000000f', { + 'targets': [ + { + 'target_name': 'ncrypto_engine', + 'type': 'static_library', + 'include_dirs': ['.'], + 'defines': [ + 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', + '<@(ncrypto_engine_defines)', + ], + 'sources': [ '<@(ncrypto_engine_sources)' ], + 'conditions': [ + ['node_shared_openssl=="false"', { + 'dependencies': [ + '../openssl/openssl.gyp:openssl' + ] + }], + ] + }, + ], + }], + ], } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 76d79652607139..92b36a15dd6250 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -19,9 +19,10 @@ #include #include #include -#ifndef OPENSSL_NO_ENGINE +#if defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT && \ + !defined(OPENSSL_NO_ENGINE) #include -#endif // !OPENSSL_NO_ENGINE +#endif // NCRYPTO_ENGINE_COMPAT && !OPENSSL_NO_ENGINE #ifndef OPENSSL_VERSION_PREREQ #define OPENSSL_VERSION_PREREQ(maj, min) \ @@ -40,6 +41,40 @@ #define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 0 #endif +// Backend split: +// - OpenSSL >= 3 uses provider APIs and hides deprecated low-level objects. +// - BoringSSL has its own API-compatible branch. +// - OpenSSL < 3 remains the legacy fallback branch. +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 0) +#define NCRYPTO_USE_OPENSSL3_PROVIDER 1 +#else +#define NCRYPTO_USE_OPENSSL3_PROVIDER 0 +#endif + +#ifdef OPENSSL_IS_BORINGSSL +#define NCRYPTO_USE_BORINGSSL 1 +#else +#define NCRYPTO_USE_BORINGSSL 0 +#endif + +#if !NCRYPTO_USE_OPENSSL3_PROVIDER && !NCRYPTO_USE_BORINGSSL +#define NCRYPTO_USE_LEGACY_OPENSSL 1 +#else +#define NCRYPTO_USE_LEGACY_OPENSSL 0 +#endif + +#if NCRYPTO_USE_BORINGSSL || NCRYPTO_USE_LEGACY_OPENSSL +#define NCRYPTO_USE_LEGACY_KEY_TYPES 1 +#else +#define NCRYPTO_USE_LEGACY_KEY_TYPES 0 +#endif + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +#include +#include +#include +#endif + // The FIPS-related functions are only available // when the OpenSSL itself was compiled with FIPS support. #if defined(OPENSSL_FIPS) && !OPENSSL_VERSION_PREREQ(3, 0) @@ -112,7 +147,6 @@ #define EVP_PKEY_ML_KEM_512 NID_ML_KEM_512 #define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 #define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 -#include #elif OPENSSL_WITH_BORINGSSL_PQC #define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 #define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 @@ -303,7 +337,9 @@ template using DeleteFnPtr = typename FunctionDeleter::Pointer; using PKCS8Pointer = DeleteFnPtr; +#if NCRYPTO_USE_LEGACY_KEY_TYPES using RSAPointer = DeleteFnPtr; +#endif using SSLSessionPointer = DeleteFnPtr; class BIOPointer; @@ -502,11 +538,23 @@ class Cipher final { class Dsa final { public: Dsa(); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + explicit Dsa(const EVP_PKEY* pkey); +#else Dsa(OSSL3_CONST DSA* dsa); +#endif NCRYPTO_DISALLOW_COPY_AND_MOVE(Dsa) +#if NCRYPTO_USE_OPENSSL3_PROVIDER + inline operator bool() const { + return dsa_; + } +#else inline operator bool() const { return dsa_ != nullptr; } +#endif +#if NCRYPTO_USE_LEGACY_KEY_TYPES inline operator OSSL3_CONST DSA*() const { return dsa_; } +#endif const BIGNUM* getP() const; const BIGNUM* getQ() const; @@ -514,7 +562,13 @@ class Dsa final { size_t getDivisorLength() const; private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + bool dsa_ = false; + DeleteFnPtr p_; + DeleteFnPtr q_; +#else OSSL3_CONST DSA* dsa_; +#endif }; // ============================================================================ @@ -523,11 +577,23 @@ class Dsa final { class Rsa final { public: Rsa(); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + explicit Rsa(const EVP_PKEY* pkey); +#else Rsa(OSSL3_CONST RSA* rsa); +#endif NCRYPTO_DISALLOW_COPY_AND_MOVE(Rsa) +#if NCRYPTO_USE_OPENSSL3_PROVIDER + inline operator bool() const { + return rsa_; + } +#else inline operator bool() const { return rsa_ != nullptr; } +#endif +#if NCRYPTO_USE_LEGACY_KEY_TYPES inline operator OSSL3_CONST RSA*() const { return rsa_; } +#endif struct PublicKey { const BIGNUM* n; @@ -561,6 +627,8 @@ class Rsa final { using CipherParams = Cipher::CipherParams; + BIOPointer derPublicKey() const; + static DataPointer encrypt(const EVPKeyPointer& key, const CipherParams& params, const Buffer in); @@ -569,20 +637,41 @@ class Rsa final { const Buffer in); private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + bool rsa_ = false; + DeleteFnPtr n_; + DeleteFnPtr e_; + DeleteFnPtr d_; + DeleteFnPtr p_; + DeleteFnPtr q_; + DeleteFnPtr dp_; + DeleteFnPtr dq_; + DeleteFnPtr qi_; + std::optional pss_params_; +#else OSSL3_CONST RSA* rsa_; +#endif }; class Ec final { public: Ec(); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + explicit Ec(const EVP_PKEY* pkey); +#else Ec(OSSL3_CONST EC_KEY* key); +#endif NCRYPTO_DISALLOW_COPY_AND_MOVE(Ec) const EC_GROUP* getGroup() const; + const EC_POINT* getPublicKey() const; + point_conversion_form_t getPointConversionForm() const; int getCurve() const; inline operator bool() const { return ec_ != nullptr; } +#if NCRYPTO_USE_LEGACY_KEY_TYPES inline operator OSSL3_CONST EC_KEY*() const { return ec_; } +#endif static int GetCurveIdFromName(const char* name); @@ -590,7 +679,13 @@ class Ec final { static bool GetCurves(GetCurveCallback callback); private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + DeleteFnPtr ec_; + DeleteFnPtr pub_; + point_conversion_form_t form_ = POINT_CONVERSION_UNCOMPRESSED; +#else OSSL3_CONST EC_KEY* ec_ = nullptr; +#endif }; // A managed pointer to a buffer of data. When destroyed the underlying @@ -926,7 +1021,11 @@ class EVPKeyPointer final { const Buffer& data); #endif static EVPKeyPointer NewDH(DHPointer&& dh); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + static EVPKeyPointer NewRSA(const Rsa& rsa); +#else static EVPKeyPointer NewRSA(RSAPointer&& rsa); +#endif enum class PKEncodingType { // RSAPublicKey / RSAPrivateKey according to PKCS#1. @@ -998,7 +1097,9 @@ class EVPKeyPointer final { bool assign(const ECKeyPointer& eckey); bool set(const ECKeyPointer& eckey); +#if NCRYPTO_USE_LEGACY_KEY_TYPES operator const EC_KEY*() const; +#endif inline bool operator==(std::nullptr_t) const noexcept { return pkey_ == nullptr; @@ -1070,29 +1171,53 @@ class DHPointer final { static DHPointer New(size_t bits, unsigned int generator); DHPointer() = default; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + explicit DHPointer(EVPKeyPointer&& key, const char* group_name = nullptr); + DHPointer(BignumPointer&& p, BignumPointer&& g, const char* group_name); +#else explicit DHPointer(DH* dh); +#endif DHPointer(DHPointer&& other) noexcept; DHPointer& operator=(DHPointer&& other) noexcept; NCRYPTO_DISALLOW_COPY(DHPointer) ~DHPointer(); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + inline bool operator==(std::nullptr_t) noexcept { + return !operator bool(); + } + inline operator bool() const { + return dh_ != nullptr || (p_ && g_); + } +#else inline bool operator==(std::nullptr_t) noexcept { return dh_ == nullptr; } inline operator bool() const { return dh_ != nullptr; } +#endif +#if NCRYPTO_USE_LEGACY_KEY_TYPES inline DH* get() const { return dh_.get(); } void reset(DH* dh = nullptr); DH* release(); +#else + inline EVP_PKEY* get() const { + return dh_.get(); + } + void reset(EVP_PKEY* dh = nullptr); + EVP_PKEY* release(); +#endif enum class CheckResult { NONE, - P_NOT_PRIME = DH_CHECK_P_NOT_PRIME, - P_NOT_SAFE_PRIME = DH_CHECK_P_NOT_SAFE_PRIME, - UNABLE_TO_CHECK_GENERATOR = DH_UNABLE_TO_CHECK_GENERATOR, - NOT_SUITABLE_GENERATOR = DH_NOT_SUITABLE_GENERATOR, - Q_NOT_PRIME = DH_CHECK_Q_NOT_PRIME, + P_NOT_PRIME = 0x01, + P_NOT_SAFE_PRIME = 0x02, + UNABLE_TO_CHECK_GENERATOR = 0x04, + NOT_SUITABLE_GENERATOR = 0x08, + Q_NOT_PRIME = 0x10, #ifndef OPENSSL_IS_BORINGSSL // Boringssl does not define the DH_CHECK_INVALID_[Q or J]_VALUE - INVALID_Q = DH_CHECK_INVALID_Q_VALUE, - INVALID_J = DH_CHECK_INVALID_J_VALUE, + INVALID_Q = 0x20, + INVALID_J = 0x40, + MODULUS_TOO_SMALL = 0x80, + MODULUS_TOO_LARGE = 0x100, #endif CHECK_FAILED = 512, }; @@ -1114,11 +1239,12 @@ class DHPointer final { CheckPublicKeyResult checkPublicKey(const BignumPointer& pub_key); DataPointer getPrime() const; + size_t getPrimeBits() const; DataPointer getGenerator() const; DataPointer getPublicKey() const; DataPointer getPrivateKey() const; bool hasPrivateKey() const; - DataPointer generateKeys() const; + DataPointer generateKeys(); DataPointer computeSecret(const BignumPointer& peer) const; bool setPublicKey(BignumPointer&& key); @@ -1130,7 +1256,16 @@ class DHPointer final { const EVPKeyPointer& theirKey); private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + DeleteFnPtr dh_; + BignumPointer p_; + BignumPointer g_; + BignumPointer pub_key_; + BignumPointer pvt_key_; + const char* group_name_ = nullptr; +#else DeleteFnPtr dh_; +#endif }; struct StackOfX509Deleter { @@ -1439,20 +1574,38 @@ class ECPointPointer final { }; class ECKeyPointer final { + friend class EVPKeyPointer; + public: ECKeyPointer(); + explicit ECKeyPointer(const EVPKeyPointer& key); +#if NCRYPTO_USE_LEGACY_KEY_TYPES explicit ECKeyPointer(EC_KEY* key); +#endif ECKeyPointer(ECKeyPointer&& other) noexcept; ECKeyPointer& operator=(ECKeyPointer&& other) noexcept; NCRYPTO_DISALLOW_COPY(ECKeyPointer) ~ECKeyPointer(); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + inline bool operator==(std::nullptr_t) noexcept { + return group_ == nullptr; + } + inline operator bool() const { + return group_ != nullptr; + } +#else inline bool operator==(std::nullptr_t) noexcept { return key_ == nullptr; } inline operator bool() const { return key_ != nullptr; } +#endif +#if NCRYPTO_USE_LEGACY_KEY_TYPES inline EC_KEY* get() const { return key_.get(); } inline operator EC_KEY*() const { return key_.get(); } void reset(EC_KEY* key = nullptr); EC_KEY* release(); +#else + void reset(); +#endif ECKeyPointer clone() const; bool setPrivateKey(const BignumPointer& priv); @@ -1460,6 +1613,7 @@ class ECKeyPointer final { bool setPublicKeyRaw(const BignumPointer& x, const BignumPointer& y); bool generate(); bool checkKey() const; + DataPointer computeSecret(const ECPointPointer& peer) const; const EC_GROUP* getGroup() const; const BIGNUM* getPrivateKey() const; @@ -1468,14 +1622,22 @@ class ECKeyPointer final { static ECKeyPointer New(const EC_GROUP* group); static ECKeyPointer NewByCurveName(int nid); +#if NCRYPTO_USE_LEGACY_KEY_TYPES static const EC_POINT* GetPublicKey(const EC_KEY* key); static const BIGNUM* GetPrivateKey(const EC_KEY* key); static const EC_GROUP* GetGroup(const EC_KEY* key); static int GetGroupName(const EC_KEY* key); static bool Check(const EC_KEY* key); +#endif private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + DeleteFnPtr group_; + DeleteFnPtr pub_; + DeleteFnPtr priv_; +#else DeleteFnPtr key_; +#endif }; class EVPMDCtxPointer final { @@ -1642,24 +1804,23 @@ class EnginePointer final { public: EnginePointer() = default; - explicit EnginePointer(ENGINE* engine_, bool finish_on_exit = false); + explicit EnginePointer(void* engine_, bool finish_on_exit = false); EnginePointer(EnginePointer&& other) noexcept; EnginePointer& operator=(EnginePointer&& other) noexcept; NCRYPTO_DISALLOW_COPY(EnginePointer) ~EnginePointer(); inline operator bool() const { return engine != nullptr; } - inline ENGINE* get() { return engine; } inline void setFinishOnExit() { finish_on_exit = true; } - void reset(ENGINE* engine_ = nullptr, bool finish_on_exit_ = false); + void reset(void* engine_ = nullptr, bool finish_on_exit_ = false); bool setAsDefault(uint32_t flags, CryptoErrorList* errors = nullptr); bool init(bool finish_on_exit = false); EVPKeyPointer loadPrivateKey(const char* key_name); + bool setClientCertEngine(SSL_CTX* ctx); - // Release ownership of the ENGINE* pointer. - ENGINE* release(); + void* release(); // Retrieve an OpenSSL Engine instance by name. If the name does not // identify a valid named engine, the returned EnginePointer will be @@ -1671,7 +1832,7 @@ class EnginePointer final { static void initEnginesOnce(); private: - ENGINE* engine = nullptr; + void* engine = nullptr; bool finish_on_exit = false; }; #endif // !OPENSSL_NO_ENGINE diff --git a/deps/ncrypto/unofficial.gni b/deps/ncrypto/unofficial.gni index 7cb27d22b9b8e0..dad4fbbf16f060 100644 --- a/deps/ncrypto/unofficial.gni +++ b/deps/ncrypto/unofficial.gni @@ -26,7 +26,11 @@ template("ncrypto_gn_build") { source_set(target_name) { forward_variables_from(invoker, "*") public_configs = [ ":ncrypto_config" ] - sources = gypi_values.ncrypto_sources + defines = [ + "NCRYPTO_ENGINE_COMPAT=1", + "OPENSSL_SUPPRESS_DEPRECATED", + ] + sources = gypi_values.ncrypto_sources + gypi_values.ncrypto_engine_sources deps = [ "$node_openssl_path" ] } } diff --git a/deps/npm/docs/content/commands/npm-approve-scripts.md b/deps/npm/docs/content/commands/npm-approve-scripts.md index a89a250a769dc1..c4d3c15a8149f2 100644 --- a/deps/npm/docs/content/commands/npm-approve-scripts.md +++ b/deps/npm/docs/content/commands/npm-approve-scripts.md @@ -59,6 +59,14 @@ the command cannot infer. Existing `false` entries always win; `approve-scripts` will not silently re-allow a package you previously denied. +If a registry dependency has no `resolved` URL in your `package-lock.json` +(for example, an older lockfile or one written with +`omit-lockfile-registry-resolved`), npm cannot verify a trusted version for +it and cannot pin it: a `pkg@1.2.3` entry never matches, so the package +keeps appearing under `--allow-scripts-pending`. `approve-scripts` approves +these by name (`pkg: true`) and warns when it does. To restore pinning, +refresh the lockfile with `npm install`. + ### Examples ```bash diff --git a/deps/npm/docs/content/commands/npm-ci.md b/deps/npm/docs/content/commands/npm-ci.md index bc460070459604..741caf5eae7026 100644 --- a/deps/npm/docs/content/commands/npm-ci.md +++ b/deps/npm/docs/content/commands/npm-ci.md @@ -74,8 +74,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). @@ -298,6 +306,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `dangerously-allow-all-scripts` diff --git a/deps/npm/docs/content/commands/npm-dedupe.md b/deps/npm/docs/content/commands/npm-dedupe.md index 8186ee2c1f31c7..a48bf1bd622e5c 100644 --- a/deps/npm/docs/content/commands/npm-dedupe.md +++ b/deps/npm/docs/content/commands/npm-dedupe.md @@ -74,8 +74,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). diff --git a/deps/npm/docs/content/commands/npm-exec.md b/deps/npm/docs/content/commands/npm-exec.md index 13a0939209a5ea..ff08d07786a8d3 100644 --- a/deps/npm/docs/content/commands/npm-exec.md +++ b/deps/npm/docs/content/commands/npm-exec.md @@ -194,6 +194,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `dangerously-allow-all-scripts` diff --git a/deps/npm/docs/content/commands/npm-find-dupes.md b/deps/npm/docs/content/commands/npm-find-dupes.md index 52bb59b9f81b57..2d72186fe5a22e 100644 --- a/deps/npm/docs/content/commands/npm-find-dupes.md +++ b/deps/npm/docs/content/commands/npm-find-dupes.md @@ -25,8 +25,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). 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 4528f63dfe28e8..2194a4df84a80c 100644 --- a/deps/npm/docs/content/commands/npm-install-ci-test.md +++ b/deps/npm/docs/content/commands/npm-install-ci-test.md @@ -27,8 +27,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). @@ -251,6 +259,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `dangerously-allow-all-scripts` diff --git a/deps/npm/docs/content/commands/npm-install-scripts.md b/deps/npm/docs/content/commands/npm-install-scripts.md new file mode 100644 index 00000000000000..4182392c9a3d1e --- /dev/null +++ b/deps/npm/docs/content/commands/npm-install-scripts.md @@ -0,0 +1,166 @@ +--- +title: npm-install-scripts +section: 1 +description: Manage install-script approvals for dependencies +--- + +### Synopsis + +```bash +npm install-scripts approve [ ...] +npm install-scripts approve --all +npm install-scripts deny [ ...] +npm install-scripts deny --all +npm install-scripts ls +npm install-scripts prune +``` + +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 is the recommended way to maintain that field. + +Dependency install scripts are blocked by default. Install commands +silently skip lifecycle scripts for any dependency that does not have a +matching entry in `allowScripts`, and end with a list of the packages +whose scripts were skipped so you can review them here. + +This command only works inside a project that has a `package.json`. Running +it with `--global` (`-g`) fails with an `EGLOBAL` error, since global +installs (`npm install -g`) and one-off executions (`npm exec` / `npx`) have +no project `package.json` to write to. To allow install scripts in those +contexts, use the `--allow-scripts` flag at install time (for example +`npm install -g --allow-scripts=canvas,sharp`) or persist the setting with +`npm config set allow-scripts=canvas,sharp --location=user`. + +There are four subcommands: + +```bash +npm install-scripts approve [ ...] +npm install-scripts approve --all +npm install-scripts deny [ ...] +npm install-scripts deny --all +npm install-scripts ls +npm install-scripts prune +``` + +`approve` allows install scripts for the named packages. `` matches +every installed version of that package. By default it 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. + +`deny` records an explicit denial for the named packages (a name-only `false` +entry), which survives `npm install-scripts approve --all` and excludes the +package from any future blanket approval. `--all` denies every package with +unreviewed install scripts. + +`ls` is read-only: it lists every package whose install scripts are not yet +covered by `allowScripts`, without modifying `package.json`. + +`prune` removes `allowScripts` entries that no longer match an installed +package with an install script, either because the package is no longer +installed (a transitive dependency changed, or a pinned `pkg@1.2.3` was +upgraded) or because it no longer has an install script. Both approvals +(`true`) and denials (`false`) are removed. It edits only the `allowScripts` +field in `package.json`, never `.npmrc` or `--allow-scripts`. Pass `--dry-run` +to preview without writing. Unparseable keys are left alone. + +`approve` 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` will not silently re-allow a package you +previously denied. + +The standalone commands [`npm approve-scripts`](/commands/npm-approve-scripts) +and [`npm deny-scripts`](/commands/npm-deny-scripts) are aliases for +`npm install-scripts approve` and `npm install-scripts deny`. + +### Examples + +```bash +# Approve all currently-installed install scripts after reviewing them +npm install-scripts approve --all + +# Approve specific packages, pinned to their installed version +npm install-scripts approve canvas sharp + +# Deny a package so it stays blocked +npm install-scripts deny telemetry-pkg + +# Preview which packages still need review +npm install-scripts ls + +# Preview stale allowScripts entries, then remove them +npm install-scripts prune --dry-run +npm install-scripts prune +``` + +### Configuration + +#### `all` + +* Default: false +* Type: Boolean + +Show or act on all packages, not just the ones your project directly depends +on. For `npm outdated` and `npm ls` this lists every outdated or installed +package. For `npm approve-scripts` and `npm deny-scripts` it selects every +package with pending install 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. + + + +#### `dry-run` + +* Default: false +* Type: 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. + + + +#### `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 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-install-test.md b/deps/npm/docs/content/commands/npm-install-test.md index 44de058c56aa05..e13f79a51e6f18 100644 --- a/deps/npm/docs/content/commands/npm-install-test.md +++ b/deps/npm/docs/content/commands/npm-install-test.md @@ -68,8 +68,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). @@ -328,6 +336,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `dangerously-allow-all-scripts` @@ -375,6 +387,10 @@ sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. +As with `min-release-age`, when this cutoff blocks a fix that `npm audit +fix` would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code. + Packages whose names match `min-release-age-exclude` are exempt from this filter. @@ -397,6 +413,12 @@ your `.npmrc` is preserved when npm internally spawns a sub-process with apply, `before` wins within a single source and across sources the standard precedence rules apply. +When this window stops `npm audit fix` from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +`min-release-age-exclude`, or relax `min-release-age` or `before`. + Packages whose names match `min-release-age-exclude` are exempt from this filter. diff --git a/deps/npm/docs/content/commands/npm-install.md b/deps/npm/docs/content/commands/npm-install.md index 5bd36c1fa45320..98d69d5e842451 100644 --- a/deps/npm/docs/content/commands/npm-install.md +++ b/deps/npm/docs/content/commands/npm-install.md @@ -410,8 +410,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). @@ -670,6 +678,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `dangerously-allow-all-scripts` @@ -717,6 +729,10 @@ sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. +As with `min-release-age`, when this cutoff blocks a fix that `npm audit +fix` would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code. + Packages whose names match `min-release-age-exclude` are exempt from this filter. @@ -739,6 +755,12 @@ your `.npmrc` is preserved when npm internally spawns a sub-process with apply, `before` wins within a single source and across sources the standard precedence rules apply. +When this window stops `npm audit fix` from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +`min-release-age-exclude`, or relax `min-release-age` or `before`. + Packages whose names match `min-release-age-exclude` are exempt from this filter. diff --git a/deps/npm/docs/content/commands/npm-link.md b/deps/npm/docs/content/commands/npm-link.md index 37efda66408fbd..fa9626c4edd804 100644 --- a/deps/npm/docs/content/commands/npm-link.md +++ b/deps/npm/docs/content/commands/npm-link.md @@ -138,8 +138,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). diff --git a/deps/npm/docs/content/commands/npm-ls.md b/deps/npm/docs/content/commands/npm-ls.md index ae8e64ada95551..fba77fd01cffaf 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.17.0 /path/to/npm +npm@11.18.0 /path/to/npm └─┬ init-package-json@0.0.4 └── promzard@0.1.5 ``` diff --git a/deps/npm/docs/content/commands/npm-outdated.md b/deps/npm/docs/content/commands/npm-outdated.md index 1f368132c465a5..40f7e835a68a51 100644 --- a/deps/npm/docs/content/commands/npm-outdated.md +++ b/deps/npm/docs/content/commands/npm-outdated.md @@ -172,6 +172,10 @@ sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. +As with `min-release-age`, when this cutoff blocks a fix that `npm audit +fix` would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code. + Packages whose names match `min-release-age-exclude` are exempt from this filter. @@ -194,6 +198,12 @@ your `.npmrc` is preserved when npm internally spawns a sub-process with apply, `before` wins within a single source and across sources the standard precedence rules apply. +When this window stops `npm audit fix` from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +`min-release-age-exclude`, or relax `min-release-age` or `before`. + Packages whose names match `min-release-age-exclude` are exempt from this filter. diff --git a/deps/npm/docs/content/commands/npm-query.md b/deps/npm/docs/content/commands/npm-query.md index 83ea73188f7f10..4349e47ad23e06 100644 --- a/deps/npm/docs/content/commands/npm-query.md +++ b/deps/npm/docs/content/commands/npm-query.md @@ -284,6 +284,10 @@ sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. +As with `min-release-age`, when this cutoff blocks a fix that `npm audit +fix` would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code. + Packages whose names match `min-release-age-exclude` are exempt from this filter. @@ -306,6 +310,12 @@ your `.npmrc` is preserved when npm internally spawns a sub-process with apply, `before` wins within a single source and across sources the standard precedence rules apply. +When this window stops `npm audit fix` from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +`min-release-age-exclude`, or relax `min-release-age` or `before`. + Packages whose names match `min-release-age-exclude` are exempt from this filter. diff --git a/deps/npm/docs/content/commands/npm-rebuild.md b/deps/npm/docs/content/commands/npm-rebuild.md index 18b1d37c779956..c70307a2a7fe2d 100644 --- a/deps/npm/docs/content/commands/npm-rebuild.md +++ b/deps/npm/docs/content/commands/npm-rebuild.md @@ -136,6 +136,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `dangerously-allow-all-scripts` diff --git a/deps/npm/docs/content/commands/npm-update.md b/deps/npm/docs/content/commands/npm-update.md index ee61e27dcda0e6..317f85f7d0dbd6 100644 --- a/deps/npm/docs/content/commands/npm-update.md +++ b/deps/npm/docs/content/commands/npm-update.md @@ -177,8 +177,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). @@ -338,6 +346,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `dangerously-allow-all-scripts` @@ -385,6 +397,10 @@ sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. +As with `min-release-age`, when this cutoff blocks a fix that `npm audit +fix` would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code. + Packages whose names match `min-release-age-exclude` are exempt from this filter. @@ -407,6 +423,12 @@ your `.npmrc` is preserved when npm internally spawns a sub-process with apply, `before` wins within a single source and across sources the standard precedence rules apply. +When this window stops `npm audit fix` from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +`min-release-age-exclude`, or relax `min-release-age` or `before`. + Packages whose names match `min-release-age-exclude` are exempt from this filter. diff --git a/deps/npm/docs/content/commands/npm.md b/deps/npm/docs/content/commands/npm.md index d924d23c0ece0e..088553c6ffc486 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.17.0 +11.18.0 ### Description diff --git a/deps/npm/docs/content/using-npm/config.md b/deps/npm/docs/content/using-npm/config.md index 6bbb68fdf56528..5d951493e8a1c1 100644 --- a/deps/npm/docs/content/using-npm/config.md +++ b/deps/npm/docs/content/using-npm/config.md @@ -349,6 +349,10 @@ sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. +As with `min-release-age`, when this cutoff blocks a fix that `npm audit +fix` would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code. + Packages whose names match `min-release-age-exclude` are exempt from this filter. @@ -1029,8 +1033,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use `--install-strategy=linked` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +`import` of a package that was never added to `package.json` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). @@ -1224,6 +1236,12 @@ your `.npmrc` is preserved when npm internally spawns a sub-process with apply, `before` wins within a single source and across sources the standard precedence rules apply. +When this window stops `npm audit fix` from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +`min-release-age-exclude`, or relax `min-release-age` or `before`. + Packages whose names match `min-release-age-exclude` are exempt from this filter. @@ -1620,7 +1638,14 @@ registry (https://registry.npmjs.org) to the configured registry. If set to "never", then use the registry value. If set to "always", then replace the registry host with the configured host every time. -You may also specify a bare hostname (e.g., "registry.npmjs.org"). +You may also specify a bare hostname (e.g., "registry.npmjs.org") to only +replace URLs coming from that host. + +You may also specify a full URL including a path (e.g., +"https://old-registry.example.com/npm/path"). In that case, resolved URLs +whose host and path begin with that prefix will have the entire prefix +replaced with the configured registry URL (host and path), without +duplicating path segments. @@ -1877,6 +1902,10 @@ silently skipped; this setting only affects unreviewed entries. `--ignore-scripts` and `--dangerously-allow-all-scripts` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching `os`, `cpu`, or `libc`) are not flagged, because +their install scripts never run. + #### `strict-peer-deps` diff --git a/deps/npm/docs/content/using-npm/developers.md b/deps/npm/docs/content/using-npm/developers.md index de0cb848c59ff4..b1fff0e3894f30 100644 --- a/deps/npm/docs/content/using-npm/developers.md +++ b/deps/npm/docs/content/using-npm/developers.md @@ -171,6 +171,24 @@ to install it locally into the node_modules folder in that other place. Then go into the node-repl, and try using require("my-thing") to bring in your module's main module. +#### Catching undeclared ("phantom") dependencies + +Under the default hoisted `node_modules` layout, your package can `import` a dependency it never declared and still resolve it. +A transitive dependency hoisted alongside it, or your workspace root's `node_modules`, happens to satisfy the `import`. +That undeclared ("phantom") dependency passes your own build silently, then fails for anyone who installs your package on its own. + +We recommend developing your package under [`install-strategy=linked`](/using-npm/config#install-strategy). +The isolated layout only exposes a package's *declared* dependencies, so an `import` of an undeclared package fails for you during development instead of resolving by accident, shipping broken, and failing for your users: + +```bash +npm install --install-strategy=linked +npm test +``` + +> **Note:** This doesn't catch every case. +> A dependency that's still satisfied at your build by a `devDependency` or by your workspace root's `node_modules` can resolve fine for you and still be missing for whoever installs your package. +> So treat it as one check, not a guarantee, alongside auditing the dependencies your published package actually uses. + ### Create a User Account Create a user with the adduser command. diff --git a/deps/npm/docs/output/commands/npm-access.html b/deps/npm/docs/output/commands/npm-access.html index fd3ccdbdf886aa..13475a5d16f425 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.17.0 + @11.18.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 56679269f19860..e43659736fec33 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.17.0 + @11.18.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 index 8d13f05fb30d4b..c3bc0172b5a4db 100644 --- a/deps/npm/docs/output/commands/npm-approve-scripts.html +++ b/deps/npm/docs/output/commands/npm-approve-scripts.html @@ -186,9 +186,9 @@
-

+

npm-approve-scripts - @11.17.0 + @11.18.0

Approve install scripts for specific dependencies
@@ -238,6 +238,13 @@

Description

the command cannot infer. Existing false entries always win; approve-scripts will not silently re-allow a package you previously denied.

+

If a registry dependency has no resolved URL in your package-lock.json +(for example, an older lockfile or one written with +omit-lockfile-registry-resolved), npm cannot verify a trusted version for +it and cannot pin it: a pkg@1.2.3 entry never matches, so the package +keeps appearing under --allow-scripts-pending. approve-scripts approves +these by name (pkg: true) and warns when it does. To restore pinning, +refresh the lockfile with npm install.

Examples

# Approve all currently-installed install scripts after reviewing them
 npm approve-scripts --all
diff --git a/deps/npm/docs/output/commands/npm-audit.html b/deps/npm/docs/output/commands/npm-audit.html
index e250ae85e00907..61042b6fbb07c0 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.17.0 + @11.18.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 9d7cabb2eb07a9..660f011d1a0d9f 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.17.0 + @11.18.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 49b091085717d2..553a38d06f42ab 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.17.0 + @11.18.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 f01388a8be4ff3..3e61cba31beaa8 100644 --- a/deps/npm/docs/output/commands/npm-ci.html +++ b/deps/npm/docs/output/commands/npm-ci.html @@ -186,9 +186,9 @@
-

+

npm-ci - @11.17.0 + @11.18.0

Clean install a project
@@ -251,8 +251,15 @@

install-strategy

(default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

+deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

+

We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

legacy-bundling

  • Default: false
  • @@ -419,6 +426,9 @@

    strict-allow-scripts

    silently skipped; this setting only affects unreviewed entries. --ignore-scripts and --dangerously-allow-all-scripts both override this setting.

    +

    Optional dependencies that cannot be installed on the current platform or +engine (a non-matching os, cpu, or libc) are not flagged, because +their install scripts never run.

    dangerously-allow-all-scripts

    • Default: false
    • diff --git a/deps/npm/docs/output/commands/npm-completion.html b/deps/npm/docs/output/commands/npm-completion.html index e0769eb5665c5a..d24da48ebb03ff 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.17.0 + @11.18.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 83b7a3b0eeab8c..a3c0dab2dee799 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.17.0 + @11.18.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 26065b3862e16c..fa8d4b7084379d 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.17.0 + @11.18.0

      Reduce duplication in the package tree
      @@ -245,8 +245,15 @@

      install-strategy

      (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

      +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

      +

      We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

      legacy-bundling

      • Default: false
      • diff --git a/deps/npm/docs/output/commands/npm-deny-scripts.html b/deps/npm/docs/output/commands/npm-deny-scripts.html index 957193be87c446..4f7653c40173a6 100644 --- a/deps/npm/docs/output/commands/npm-deny-scripts.html +++ b/deps/npm/docs/output/commands/npm-deny-scripts.html @@ -186,9 +186,9 @@
        -

        +

        npm-deny-scripts - @11.17.0 + @11.18.0

        Deny install scripts for specific dependencies
        diff --git a/deps/npm/docs/output/commands/npm-deprecate.html b/deps/npm/docs/output/commands/npm-deprecate.html index b58d5388a985b9..30c4c912238480 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.17.0 + @11.18.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 a092efb1faf40c..6cf770360c1076 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.17.0 + @11.18.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 2fc358fef37ca3..59985e4a43f316 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.17.0 + @11.18.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 2eaade58d4d9ae..884f20d70d2f93 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.17.0 + @11.18.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 e7a3771d5d654e..13bb25228dd28b 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.17.0 + @11.18.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 6f1ee44d54b0fe..6d1215ebfe1038 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.17.0 + @11.18.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 d64cf7d587da27..3f57293a17fc56 100644 --- a/deps/npm/docs/output/commands/npm-exec.html +++ b/deps/npm/docs/output/commands/npm-exec.html @@ -186,9 +186,9 @@
        -

        +

        npm-exec - @11.17.0 + @11.18.0

        Run a command from a local or remote npm package
        @@ -336,6 +336,9 @@

        strict-allow-scripts

        silently skipped; this setting only affects unreviewed entries. --ignore-scripts and --dangerously-allow-all-scripts both override this setting.

        +

        Optional dependencies that cannot be installed on the current platform or +engine (a non-matching os, cpu, or libc) are not flagged, because +their install scripts never run.

        dangerously-allow-all-scripts

        • Default: false
        • diff --git a/deps/npm/docs/output/commands/npm-explain.html b/deps/npm/docs/output/commands/npm-explain.html index 4c61e4dc5fbf14..5d5f6cea5ce113 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.17.0 + @11.18.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 68f61b31ed7d1b..d1ecb4e81886a8 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.17.0 + @11.18.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 c6dea14d12f997..28a9066398760c 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.17.0 + @11.18.0

          Find duplication in the package tree
          @@ -213,8 +213,15 @@

          install-strategy

          (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

          +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

          +

          We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

          legacy-bundling

          • Default: false
          • diff --git a/deps/npm/docs/output/commands/npm-fund.html b/deps/npm/docs/output/commands/npm-fund.html index 0b7602251f020f..26761cc90196c8 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.17.0 + @11.18.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 e81310c39b33c0..e8fe5dfc0373ea 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.17.0 + @11.18.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 b44eddadf701b0..24f4ddafaea589 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.17.0 + @11.18.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 d22edb7c826b09..4fb2ef4be76c30 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.17.0 + @11.18.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 836969ed135304..deeaf25a2348a0 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.17.0 + @11.18.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 6f633c2c18a02d..2227da94885950 100644 --- a/deps/npm/docs/output/commands/npm-install-ci-test.html +++ b/deps/npm/docs/output/commands/npm-install-ci-test.html @@ -186,9 +186,9 @@
            -

            +

            npm-install-ci-test - @11.17.0 + @11.18.0

            Install a project with a clean slate and run tests
            @@ -215,8 +215,15 @@

            install-strategy

            (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

            +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

            +

            We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

            legacy-bundling

            • Default: false
            • @@ -383,6 +390,9 @@

              strict-allow-scripts

              silently skipped; this setting only affects unreviewed entries. --ignore-scripts and --dangerously-allow-all-scripts both override this setting.

              +

              Optional dependencies that cannot be installed on the current platform or +engine (a non-matching os, cpu, or libc) are not flagged, because +their install scripts never run.

              dangerously-allow-all-scripts

              • Default: false
              • diff --git a/deps/npm/docs/output/commands/npm-install-scripts.html b/deps/npm/docs/output/commands/npm-install-scripts.html new file mode 100644 index 00000000000000..1b036581f6a36e --- /dev/null +++ b/deps/npm/docs/output/commands/npm-install-scripts.html @@ -0,0 +1,341 @@ + + +npm-install-scripts + + + + + +
                +
                +

                + npm-install-scripts + @11.18.0 +

                +Manage install-script approvals for dependencies +
                + +
                +

                Table of contents

                + +
                + +

                Synopsis

                +
                npm install-scripts approve <pkg> [<pkg> ...]
                +npm install-scripts approve --all
                +npm install-scripts deny <pkg> [<pkg> ...]
                +npm install-scripts deny --all
                +npm install-scripts ls
                +npm install-scripts prune
                +
                +

                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 is the recommended way to maintain that field.

                +

                Dependency install scripts are blocked by default. Install commands +silently skip lifecycle scripts for any dependency that does not have a +matching entry in allowScripts, and end with a list of the packages +whose scripts were skipped so you can review them here.

                +

                This command only works inside a project that has a package.json. Running +it with --global (-g) fails with an EGLOBAL error, since global +installs (npm install -g) and one-off executions (npm exec / npx) have +no project package.json to write to. To allow install scripts in those +contexts, use the --allow-scripts flag at install time (for example +npm install -g --allow-scripts=canvas,sharp) or persist the setting with +npm config set allow-scripts=canvas,sharp --location=user.

                +

                There are four subcommands:

                +
                npm install-scripts approve <pkg> [<pkg> ...]
                +npm install-scripts approve --all
                +npm install-scripts deny <pkg> [<pkg> ...]
                +npm install-scripts deny --all
                +npm install-scripts ls
                +npm install-scripts prune
                +
                +

                approve allows install scripts for the named packages. <pkg> matches +every installed version of that package. By default it 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.

                +

                deny records an explicit denial for the named packages (a name-only false +entry), which survives npm install-scripts approve --all and excludes the +package from any future blanket approval. --all denies every package with +unreviewed install scripts.

                +

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

                +

                prune removes allowScripts entries that no longer match an installed +package with an install script, either because the package is no longer +installed (a transitive dependency changed, or a pinned pkg@1.2.3 was +upgraded) or because it no longer has an install script. Both approvals +(true) and denials (false) are removed. It edits only the allowScripts +field in package.json, never .npmrc or --allow-scripts. Pass --dry-run +to preview without writing. Unparseable keys are left alone.

                +

                approve 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 will not silently re-allow a package you +previously denied.

                +

                The standalone commands npm approve-scripts +and npm deny-scripts are aliases for +npm install-scripts approve and npm install-scripts deny.

                +

                Examples

                +
                # Approve all currently-installed install scripts after reviewing them
                +npm install-scripts approve --all
                +
                +# Approve specific packages, pinned to their installed version
                +npm install-scripts approve canvas sharp
                +
                +# Deny a package so it stays blocked
                +npm install-scripts deny telemetry-pkg
                +
                +# Preview which packages still need review
                +npm install-scripts ls
                +
                +# Preview stale allowScripts entries, then remove them
                +npm install-scripts prune --dry-run
                +npm install-scripts prune
                +
                +

                Configuration

                +

                all

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

                Show or act on all packages, not just the ones your project directly depends +on. For npm outdated and npm ls this lists every outdated or installed +package. For npm approve-scripts and npm deny-scripts it selects every +package with pending install 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.

                +

                dry-run

                +
                  +
                • Default: false
                • +
                • Type: 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.

                +

                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-install-test.html b/deps/npm/docs/output/commands/npm-install-test.html index bd12094e8ca1c6..94231076345ad4 100644 --- a/deps/npm/docs/output/commands/npm-install-test.html +++ b/deps/npm/docs/output/commands/npm-install-test.html @@ -186,9 +186,9 @@
                -

                +

                npm-install-test - @11.17.0 + @11.18.0

                Install package(s) and run tests
                @@ -246,8 +246,15 @@

                install-strategy

                (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

                +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

                +

                We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

                legacy-bundling

                • Default: false
                • @@ -439,6 +446,9 @@

                  strict-allow-scripts

                  silently skipped; this setting only affects unreviewed entries. --ignore-scripts and --dangerously-allow-all-scripts both override this setting.

                  +

                  Optional dependencies that cannot be installed on the current platform or +engine (a non-matching os, cpu, or libc) are not flagged, because +their install scripts never run.

                  dangerously-allow-all-scripts

                  • Default: false
                  • @@ -475,6 +485,8 @@

                    before

                    sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one.

                    +

                    As with min-release-age, when this cutoff blocks a fix that npm audit fix would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code.

                    Packages whose names match min-release-age-exclude are exempt from this filter.

                    min-release-age

                    @@ -492,6 +504,11 @@

                    min-release-age

                    --before while preparing a git: or github: dependency); when both apply, before wins within a single source and across sources the standard precedence rules apply.

                    +

                    When this window stops npm audit fix from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +min-release-age-exclude, or relax min-release-age or before.

                    Packages whose names match min-release-age-exclude are exempt from this filter.

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

                    diff --git a/deps/npm/docs/output/commands/npm-install.html b/deps/npm/docs/output/commands/npm-install.html index 767a65f28bbd2c..fd6fe12eb8cd6d 100644 --- a/deps/npm/docs/output/commands/npm-install.html +++ b/deps/npm/docs/output/commands/npm-install.html @@ -186,9 +186,9 @@
                    -

                    +

                    npm-install - @11.17.0 + @11.18.0

                    Install a package
                    @@ -521,8 +521,15 @@

                    install-strategy

                    (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

                    +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

                    +

                    We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

                    legacy-bundling

                    • Default: false
                    • @@ -714,6 +721,9 @@

                      strict-allow-scripts

                      silently skipped; this setting only affects unreviewed entries. --ignore-scripts and --dangerously-allow-all-scripts both override this setting.

                      +

                      Optional dependencies that cannot be installed on the current platform or +engine (a non-matching os, cpu, or libc) are not flagged, because +their install scripts never run.

                      dangerously-allow-all-scripts

                      • Default: false
                      • @@ -750,6 +760,8 @@

                        before

                        sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one.

                        +

                        As with min-release-age, when this cutoff blocks a fix that npm audit fix would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code.

                        Packages whose names match min-release-age-exclude are exempt from this filter.

                        min-release-age

                        @@ -767,6 +779,11 @@

                        min-release-age

                        --before while preparing a git: or github: dependency); when both apply, before wins within a single source and across sources the standard precedence rules apply.

                        +

                        When this window stops npm audit fix from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +min-release-age-exclude, or relax min-release-age or before.

                        Packages whose names match min-release-age-exclude are exempt from this filter.

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

                        diff --git a/deps/npm/docs/output/commands/npm-link.html b/deps/npm/docs/output/commands/npm-link.html index 42fd82304ecf17..e4c9f092f37c09 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.17.0 + @11.18.0

                        Symlink a package folder
                        @@ -288,8 +288,15 @@

                        install-strategy

                        (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

                        +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

                        +

                        We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

                        legacy-bundling

                        • Default: false
                        • diff --git a/deps/npm/docs/output/commands/npm-ll.html b/deps/npm/docs/output/commands/npm-ll.html index e288cc8a65d634..a7e3061ddf98bf 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.17.0 + @11.18.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 7ead867b8d396c..b08ac7b3357030 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.17.0 + @11.18.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 19ecf24f01efad..dfc1b256538f45 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.17.0 + @11.18.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 181541d978ea07..a3de66d0323c8e 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.17.0 + @11.18.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.17.0 /path/to/npm
                          +
                          npm@11.18.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 361f44be6f3040..7c0526d750c62f 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.17.0 + @11.18.0

                          Manage orgs
                          diff --git a/deps/npm/docs/output/commands/npm-outdated.html b/deps/npm/docs/output/commands/npm-outdated.html index 9703cf4bacf71f..5bf8ee630c262c 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.17.0 + @11.18.0

                          Check for outdated packages
                          @@ -334,6 +334,8 @@

                          before

                          sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one.

                          +

                          As with min-release-age, when this cutoff blocks a fix that npm audit fix would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code.

                          Packages whose names match min-release-age-exclude are exempt from this filter.

                          min-release-age

                          @@ -351,6 +353,11 @@

                          min-release-age

                          --before while preparing a git: or github: dependency); when both apply, before wins within a single source and across sources the standard precedence rules apply.

                          +

                          When this window stops npm audit fix from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +min-release-age-exclude, or relax min-release-age or before.

                          Packages whose names match min-release-age-exclude are exempt from this filter.

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

                          diff --git a/deps/npm/docs/output/commands/npm-owner.html b/deps/npm/docs/output/commands/npm-owner.html index 649e7408130966..c4947a01ae92a3 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.17.0 + @11.18.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 ab45154d21a3e2..a875a527f81f37 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.17.0 + @11.18.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 2f2ad0f779fa0a..74a7e2adc927e4 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.17.0 + @11.18.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 7462527a590499..9aec2f5f560235 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.17.0 + @11.18.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 3fe3a77f2baf7c..887a4f26432c3c 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.17.0 + @11.18.0

                          Display prefix
                          diff --git a/deps/npm/docs/output/commands/npm-profile.html b/deps/npm/docs/output/commands/npm-profile.html index 9a03fa2579618f..c45be8a6052c7f 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.17.0 + @11.18.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 539533b3e9a824..bfb3914265bd60 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.17.0 + @11.18.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 4200a9105d2376..f2a7ba05905630 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.17.0 + @11.18.0

                          Publish a package
                          diff --git a/deps/npm/docs/output/commands/npm-query.html b/deps/npm/docs/output/commands/npm-query.html index 862a6a6ad62f9d..7d9bf80b88d216 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.17.0 + @11.18.0

                          Dependency selector query
                          @@ -432,6 +432,8 @@

                          before

                          sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one.

                          +

                          As with min-release-age, when this cutoff blocks a fix that npm audit fix would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code.

                          Packages whose names match min-release-age-exclude are exempt from this filter.

                          min-release-age

                          @@ -449,6 +451,11 @@

                          min-release-age

                          --before while preparing a git: or github: dependency); when both apply, before wins within a single source and across sources the standard precedence rules apply.

                          +

                          When this window stops npm audit fix from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +min-release-age-exclude, or relax min-release-age or before.

                          Packages whose names match min-release-age-exclude are exempt from this filter.

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

                          diff --git a/deps/npm/docs/output/commands/npm-rebuild.html b/deps/npm/docs/output/commands/npm-rebuild.html index 822091ab35b0ed..5245b156b685ce 100644 --- a/deps/npm/docs/output/commands/npm-rebuild.html +++ b/deps/npm/docs/output/commands/npm-rebuild.html @@ -186,9 +186,9 @@
                          -

                          +

                          npm-rebuild - @11.17.0 + @11.18.0

                          Rebuild a package
                          @@ -298,6 +298,9 @@

                          strict-allow-scripts

                          silently skipped; this setting only affects unreviewed entries. --ignore-scripts and --dangerously-allow-all-scripts both override this setting.

                          +

                          Optional dependencies that cannot be installed on the current platform or +engine (a non-matching os, cpu, or libc) are not flagged, because +their install scripts never run.

                          dangerously-allow-all-scripts

                          • Default: false
                          • diff --git a/deps/npm/docs/output/commands/npm-repo.html b/deps/npm/docs/output/commands/npm-repo.html index c18499bd35e937..c210a4efd30802 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.17.0 + @11.18.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 e9be582f21a6b0..cc0ee51c458eee 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.17.0 + @11.18.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 2aaae514e72d49..6f0af8585373f7 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.17.0 + @11.18.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 4036f6e0153b30..c7119fea964e7f 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.17.0 + @11.18.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 be951feb19036c..70eb4780c5c9e5 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.17.0 + @11.18.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 8b397932b4bd7c..b417f9ba815c2b 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.17.0 + @11.18.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 4be22387a40786..dfd53bcf72f287 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.17.0 + @11.18.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 83e1f76e657287..8989040dd9f6c5 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.17.0 + @11.18.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 7a72369ac86e7a..de76caf167e94b 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.17.0 + @11.18.0

                            Stage packages for publishing
                            diff --git a/deps/npm/docs/output/commands/npm-star.html b/deps/npm/docs/output/commands/npm-star.html index a58d3d91913c47..0642cfefb4bc83 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.17.0 + @11.18.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 b13d859bffac06..3f586b4fb0a84a 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.17.0 + @11.18.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 0c88d59c649266..35a7c7798f85d4 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.17.0 + @11.18.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 12b8779cc221e9..37c8d91d4ba01b 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.17.0 + @11.18.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 9e311e6d52ad51..66263e4f1f5327 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.17.0 + @11.18.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 dddced02bd1fd4..b9e84c47671532 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.17.0 + @11.18.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 45962d19ed7b8f..c3c59a86cabe85 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.17.0 + @11.18.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 761283abca7cc1..00d389d044c840 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.17.0 + @11.18.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 ccabc141b24e8f..06ca5b07fbb355 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.17.0 + @11.18.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 e15dd8bfa7dcd8..f82cbbc1dcec2b 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.17.0 + @11.18.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 df9f218803c022..f6557a7a40abef 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.17.0 + @11.18.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 fbe3311b6f7155..d91decf5f96356 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.17.0 + @11.18.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 dc8794bcb36ed1..9b395086246d71 100644 --- a/deps/npm/docs/output/commands/npm-update.html +++ b/deps/npm/docs/output/commands/npm-update.html @@ -186,9 +186,9 @@
                            -

                            +

                            npm-update - @11.17.0 + @11.18.0

                            Update packages
                            @@ -317,8 +317,15 @@

                            install-strategy

                            (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

                            +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

                            +

                            We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

                            legacy-bundling

                            • Default: false
                            • @@ -435,6 +442,9 @@

                              strict-allow-scripts

                              silently skipped; this setting only affects unreviewed entries. --ignore-scripts and --dangerously-allow-all-scripts both override this setting.

                              +

                              Optional dependencies that cannot be installed on the current platform or +engine (a non-matching os, cpu, or libc) are not flagged, because +their install scripts never run.

                              dangerously-allow-all-scripts

                              • Default: false
                              • @@ -471,6 +481,8 @@

                                before

                                sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one.

                                +

                                As with min-release-age, when this cutoff blocks a fix that npm audit fix would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code.

                                Packages whose names match min-release-age-exclude are exempt from this filter.

                                min-release-age

                                @@ -488,6 +500,11 @@

                                min-release-age

                                --before while preparing a git: or github: dependency); when both apply, before wins within a single source and across sources the standard precedence rules apply.

                                +

                                When this window stops npm audit fix from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +min-release-age-exclude, or relax min-release-age or before.

                                Packages whose names match min-release-age-exclude are exempt from this filter.

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

                                diff --git a/deps/npm/docs/output/commands/npm-version.html b/deps/npm/docs/output/commands/npm-version.html index ca53ca723f3dd7..17e504dd49ef3b 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.17.0 + @11.18.0

                                Bump a package version
                                diff --git a/deps/npm/docs/output/commands/npm-view.html b/deps/npm/docs/output/commands/npm-view.html index 24b933c2bcd11c..6424eed09df9d2 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.17.0 + @11.18.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 6cbef18cbef7ba..9830a8c3c8d2f3 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.17.0 + @11.18.0

                                Display npm username
                                diff --git a/deps/npm/docs/output/commands/npm.html b/deps/npm/docs/output/commands/npm.html index 1e26458f7cf02a..cef7c6a5f2c88e 100644 --- a/deps/npm/docs/output/commands/npm.html +++ b/deps/npm/docs/output/commands/npm.html @@ -186,9 +186,9 @@
                                -

                                +

                                npm - @11.17.0 + @11.18.0

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

                                Table of contents

                          Note: This command is unaware of workspaces.

                          Version

                          -

                          11.17.0

                          +

                          11.18.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 8cd5c4dcaf6302..f06143a5c2cd56 100644 --- a/deps/npm/docs/output/commands/npx.html +++ b/deps/npm/docs/output/commands/npx.html @@ -186,9 +186,9 @@
                          -

                          +

                          npx - @11.17.0 + @11.18.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 6523b966186e45..78f9f0f1ebd7e9 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.17.0 + @11.18.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 21589fead823df..9ae64ab1d402dd 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.17.0 + @11.18.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 6523b966186e45..78f9f0f1ebd7e9 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.17.0 + @11.18.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 bccbdcc5d32753..cb9a3626a0ce04 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.17.0 + @11.18.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 0b58b9608ecda9..ecbc9d9e1502ec 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.17.0 + @11.18.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 1bea346797ae61..bad73af8064b6d 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.17.0 + @11.18.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 bccbdcc5d32753..cb9a3626a0ce04 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.17.0 + @11.18.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 25d262fadf53da..834e14f626389a 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.17.0 + @11.18.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 17e7bd13896348..f52caeefd1847d 100644 --- a/deps/npm/docs/output/using-npm/config.html +++ b/deps/npm/docs/output/using-npm/config.html @@ -186,9 +186,9 @@
                          -

                          +

                          Config - @11.17.0 + @11.18.0

                          About npm configuration
                          @@ -464,6 +464,8 @@

                          before

                          sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one.

                          +

                          As with min-release-age, when this cutoff blocks a fix that npm audit fix would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code.

                          Packages whose names match min-release-age-exclude are exempt from this filter.

                          @@ -948,8 +950,15 @@

                          install-strategy

                          (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted.

                          +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted.

                          +

                          We recommend that package authors use --install-strategy=linked during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +import of a package that was never added to package.json can fail +instead of resolving by accident and shipping broken. See Catching +undeclared ("phantom") +dependencies.

                          json

to install it locally into the node_modules folder in that other place.

Then go into the node-repl, and try using require("my-thing") to bring in your module's main module.

+

Catching undeclared ("phantom") dependencies

+

Under the default hoisted node_modules layout, your package can import a dependency it never declared and still resolve it. +A transitive dependency hoisted alongside it, or your workspace root's node_modules, happens to satisfy the import. +That undeclared ("phantom") dependency passes your own build silently, then fails for anyone who installs your package on its own.

+

We recommend developing your package under install-strategy=linked. +The isolated layout only exposes a package's declared dependencies, so an import of an undeclared package fails for you during development instead of resolving by accident, shipping broken, and failing for your users:

+
npm install --install-strategy=linked
+npm test
+
+
+

Note: This doesn't catch every case. +A dependency that's still satisfied at your build by a devDependency or by your workspace root's node_modules can resolve fine for you and still be missing for whoever installs your package. +So treat it as one check, not a guarantee, alongside auditing the dependencies your published package actually uses.

+

Create a User Account

Create a user with the adduser command. It works like this:

diff --git a/deps/npm/docs/output/using-npm/logging.html b/deps/npm/docs/output/using-npm/logging.html index bbba11e5508a6b..19c47d4adc3906 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.17.0 + @11.18.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 ae4920f10c3ca4..3444f3bf1c4c97 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.17.0 + @11.18.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 47d02ec2b6b94f..92cd9d2af617d8 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.17.0 + @11.18.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 2c221dee07a0ed..ed47e3a6ed7171 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.17.0 + @11.18.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 932d43121c9dea..57f02ed1ee2b7f 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.17.0 + @11.18.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 657021503ff52c..11861347920567 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.17.0 + @11.18.0

Scoped packages
diff --git a/deps/npm/docs/output/using-npm/scripts.html b/deps/npm/docs/output/using-npm/scripts.html index 3a87423e11f529..65ff19a4673afd 100644 --- a/deps/npm/docs/output/using-npm/scripts.html +++ b/deps/npm/docs/output/using-npm/scripts.html @@ -186,9 +186,9 @@
-

+

Scripts - @11.17.0 + @11.18.0

How npm handles the "scripts" field
diff --git a/deps/npm/docs/output/using-npm/workspaces.html b/deps/npm/docs/output/using-npm/workspaces.html index 2e5ac47adfc8a4..fd75a0ad62a641 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.17.0 + @11.18.0

Working with workspaces
diff --git a/deps/npm/lib/commands/audit.js b/deps/npm/lib/commands/audit.js index a8d742cc73a6e4..6a55af43135840 100644 --- a/deps/npm/lib/commands/audit.js +++ b/deps/npm/lib/commands/audit.js @@ -73,6 +73,19 @@ class Audit extends ArboristWorkspaceCmd { await arb.audit({ fix }) if (fix) { await reifyFinish(this.npm, arb) + // Report any fix that a `min-release-age`/`before` window blocked from + // installing, and exit non-zero so a blocked fix is not missed. + const report = arb.auditReport + const blocked = report instanceof Map + ? [...report.values()].filter(v => v.fixBlockedByReleaseAge).map(v => v.name) + : [] + if (blocked.length) { + log.warn('audit', `${blocked.length} package(s) left at a vulnerable version because ` + + `a fix is newer than the release-age cutoff: ${blocked.join(', ')}.\n` + + 'Add the package to min-release-age-exclude, or relax min-release-age or before, ' + + 'to install the fix.') + process.exitCode = 1 + } } else { // will throw if there's an error, because this is an audit command auditError(this.npm, arb.auditReport) diff --git a/deps/npm/lib/commands/exec.js b/deps/npm/lib/commands/exec.js index 23c47a0cc1ad77..7b7d8ad30fcd55 100644 --- a/deps/npm/lib/commands/exec.js +++ b/deps/npm/lib/commands/exec.js @@ -42,7 +42,7 @@ class Exec extends BaseCommand { } } - async callExec (args, { name, locationMsg, runPath } = {}) { + async callExec (args, { locationMsg, runPath } = {}) { let localBin = this.npm.localBin let pkgPath = this.npm.localPrefix @@ -50,8 +50,8 @@ class Exec extends BaseCommand { if (!runPath) { runPath = process.cwd() } else { - // We have to consider if the workspace has its own separate versions libnpmexec will walk up to localDir after looking here - localBin = resolve(this.npm.localDir, name, 'node_modules', '.bin') + // Use the workspace's own node_modules/.bin, not localDir/, since the linked strategy does not symlink workspaces into the root node_modules. + localBin = resolve(runPath, 'node_modules', '.bin') // We also need to look for `bin` entries in the workspace package.json // libnpmexec will NOT look in the project root for the bin entry pkgPath = runPath diff --git a/deps/npm/lib/commands/install-scripts.js b/deps/npm/lib/commands/install-scripts.js new file mode 100644 index 00000000000000..c6f91f87b08765 --- /dev/null +++ b/deps/npm/lib/commands/install-scripts.js @@ -0,0 +1,53 @@ +const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js') + +// Namespaced front-end for install-script approvals. +// `approve`/`deny` write the `allowScripts` policy, `ls` lists unreviewed packages, +// `prune` drops entries that no longer match an installed package with an install script. +// `npm approve-scripts` / `npm deny-scripts` are aliases for `approve` / `deny`. +class InstallScripts extends AllowScriptsCmd { + static description = 'Manage install-script approvals for dependencies' + static name = 'install-scripts' + static usage = [ + 'approve [ ...]', + 'approve --all', + 'deny [ ...]', + 'deny --all', + 'ls', + 'prune', + ] + + static params = ['all', 'allow-scripts-pin', 'dry-run', 'json'] + + static async completion (opts) { + const argv = opts.conf.argv.remain + const subcommands = ['approve', 'deny', 'ls', 'prune'] + if (argv.length === 2) { + return subcommands + } + if (subcommands.includes(argv[2])) { + return [] + } + throw new Error(`${argv[2]} not recognized`) + } + + async exec (args) { + const [sub, ...rest] = args + switch (sub) { + case 'approve': + return this.runMode('approve', rest) + case 'deny': + return this.runMode('deny', rest) + case 'ls': + case 'list': + return this.runMode('list', rest) + case 'prune': + return this.runMode('prune', rest) + default: + throw this.usageError( + sub ? `\`${sub}\` is not a recognized subcommand.` : undefined + ) + } + } +} + +module.exports = InstallScripts diff --git a/deps/npm/lib/commands/link.js b/deps/npm/lib/commands/link.js index 160ba2b707efd8..cad499b2df8efb 100644 --- a/deps/npm/lib/commands/link.js +++ b/deps/npm/lib/commands/link.js @@ -5,6 +5,7 @@ const pkgJson = require('@npmcli/package-json') const semver = require('semver') 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 Link extends ArboristWorkspaceCmd { @@ -68,12 +69,16 @@ class Link extends ArboristWorkspaceCmd { // load current packages from the global space, and then add symlinks installs locally const globalTop = resolve(this.npm.globalDir, '..') const Arborist = require('@npmcli/arborist') + // Resolve the policy up front so it also gates the global install of + // missing packages, not just the local link. + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const globalOpts = { ...this.npm.flatOptions, Arborist, path: globalTop, global: true, prune: false, + allowScripts: allowScriptsPolicy, } const globalArb = new Arborist(globalOpts) @@ -86,10 +91,17 @@ class Link extends ArboristWorkspaceCmd { // any extra arg that is missing from the current global space should be reified there first const missing = this.missingArgsFromTree(globals, args) if (missing.length) { - await globalArb.reify({ + const globalReifyOpts = { ...globalOpts, add: missing, + } + // Gate the global install with the same preflight as `npm install`. + await strictAllowScriptsPreflight({ + arb: globalArb, + npm: this.npm, + idealTreeOpts: globalReifyOpts, }) + await globalArb.reify(globalReifyOpts) } // get a list of module names that should be linked in the local prefix @@ -116,12 +128,13 @@ class Link extends ArboristWorkspaceCmd { ) // create a new arborist instance for the local prefix and // reify all the pending names as symlinks there - const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const localArb = new Arborist({ ...this.npm.flatOptions, prune: false, path: this.npm.prefix, save, + // Arborist reads this.options.workspaces (set at construction) to decide which node receives the add, so it must be set here, not only at reify time. + workspaces: this.workspaceNames, allowScripts: allowScriptsPolicy, }) await localArb.reify({ diff --git a/deps/npm/lib/commands/ls.js b/deps/npm/lib/commands/ls.js index 5dacd3919882e8..8b9b4f3f5d2e2d 100644 --- a/deps/npm/lib/commands/ls.js +++ b/deps/npm/lib/commands/ls.js @@ -135,7 +135,7 @@ class LS extends ArboristWorkspaceCmd { omit, }) : () => true) .filter(installStrategy === 'linked' - ? filterLinkedStrategyEdges({ node, currentDepth }) + ? filterLinkedStrategyEdges({ currentDepth }) : () => true) .map(mapEdgesToNodes({ seenPaths })) .concat(appendExtraneousChildren({ node, seenPaths })) @@ -400,32 +400,16 @@ const getJsonOutputItem = (node, { global, long }) => { return augmentItemWithIncludeMetadata(node, item) } -// In linked strategy, two types of edges produce false UNMET DEPENDENCYs: -// 1. Workspace edges for undeclared workspaces: the lockfile records edges from root to ALL workspaces, but only declared workspaces are hoisted to root/node_modules in linked mode. Undeclared ones are intentionally absent. -// 2. Dev edges on non-root packages: store package link targets have no parent in the node tree, so they are treated as "top" nodes and their devDependencies are loaded as edges. Those devDeps are never installed. -const filterLinkedStrategyEdges = ({ node, currentDepth }) => { - const declaredDeps = new Set(Object.keys(Object.assign({}, - node.target.package.dependencies, - node.target.package.devDependencies, - node.target.package.optionalDependencies, - node.target.package.peerDependencies - ))) - - return (edge) => { - // Skip workspace edges for undeclared workspaces at root level - if (currentDepth === 0 && edge.type === 'workspace' && edge.missing) { - if (!declaredDeps.has(edge.name)) { - return false - } - } - - // Skip dev edges for non-root packages (store packages) - if (currentDepth > 0 && edge.dev) { - return false - } - - return true +// In linked strategy, dev edges on non-root packages produce false UNMET DEPENDENCYs: store package link targets have no parent in the node tree, so they are treated as "top" nodes and their devDependencies are loaded as edges. Those devDeps are never installed. +// Undeclared workspaces no longer need filtering here: loadActual synthesizes their root links so their edges resolve instead of reporting missing. +const filterLinkedStrategyEdges = ({ currentDepth }) => (edge) => { + // Skip dev edges for non-root packages (store packages) + /* istanbul ignore next: store packages no longer carry dev edges, so this guard is not exercised by tests */ + if (currentDepth > 0 && edge.dev) { + return false } + + return true } const filterByEdgesTypes = ({ link, omit }) => (edge) => { diff --git a/deps/npm/lib/commands/query.js b/deps/npm/lib/commands/query.js index 63bd00c3206b33..f826cdc52659e5 100644 --- a/deps/npm/lib/commands/query.js +++ b/deps/npm/lib/commands/query.js @@ -2,6 +2,15 @@ const { resolve } = require('node:path') const BaseCommand = require('../base-cmd.js') const { log, output } = require('proc-log') +// Ranks competing representations of the same physical package so the most logical one is reported. +// A top-level placement (e.g. node_modules/) beats the canonical store node, which beats an internal store symlink. +const locationRank = (node) => { + if (!node.location.includes('node_modules/.store/')) { + return 2 + } + return node.isLink ? 0 : 1 +} + class QuerySelectorItem { constructor (node) { // all enumerable properties from the target @@ -9,8 +18,11 @@ class QuerySelectorItem { // append extra info this.pkgid = node.target.pkgid - this.location = node.target.location - this.path = node.target.path + // For a dep symlinked into the isolated store, report the logical link location (node_modules/) rather than the .store backing path. + // Workspaces and regular nodes keep the target location (e.g. packages/). + const logical = node.target.isInStore ? node : node.target + this.location = logical.location + this.path = logical.path this.realpath = node.target.realpath this.resolved = node.target.resolved this.from = [] @@ -33,7 +45,7 @@ class QuerySelectorItem { class Query extends BaseCommand { #response = [] // response is the query response - #seen = new Set() // paths we've seen so we can keep response deduped + #seen = new Map() // physical location -> index in #response, to keep response deduped static description = 'Retrieve a filtered list of packages' static name = 'query' @@ -127,12 +139,22 @@ class Query extends BaseCommand { async #queryTree (tree, arg) { const items = await tree.querySelectorAll(arg, this.npm.flatOptions) for (const node of items) { + // Dedup by the target's physical location so multiple logical links to the same store node collapse to one result. const { location } = node.target - if (!location || !this.#seen.has(location)) { - const item = new QuerySelectorItem(node) - this.#response.push(item) - if (location) { - this.#seen.add(item.location) + if (!location) { + this.#response.push(new QuerySelectorItem(node)) + continue + } + const seen = this.#seen.get(location) + if (seen === undefined) { + this.#seen.set(location, { index: this.#response.length, rank: locationRank(node) }) + this.#response.push(new QuerySelectorItem(node)) + } else { + // Replace the stored representation only with a more logical one for the same physical package. + const rank = locationRank(node) + if (rank > seen.rank) { + this.#response[seen.index] = new QuerySelectorItem(node) + seen.rank = rank } } } diff --git a/deps/npm/lib/commands/rebuild.js b/deps/npm/lib/commands/rebuild.js index 3f8cb98154d552..17595aa4eb72e2 100644 --- a/deps/npm/lib/commands/rebuild.js +++ b/deps/npm/lib/commands/rebuild.js @@ -75,13 +75,13 @@ class Rebuild extends ArboristWorkspaceCmd { if (unreviewed.length > 0) { const count = unreviewed.length const noun = count === 1 ? 'package has' : 'packages have' - // `npm approve-scripts` writes to a project package.json, which doesn't + // `npm install-scripts` writes to a project package.json, which doesn't // exist for global rebuilds. Point global users at `npm config set`, // which writes the `allow-scripts` setting to their user .npmrc. const names = unreviewed.map(({ node }) => trustedDisplay(node).name) const remediation = this.npm.global ? `Run \`${configSetAllowScripts(names)}\` to allow their scripts.` - : 'Run `npm approve-scripts --allow-scripts-pending` to review.' + : 'Run `npm install-scripts ls` to review.' log.warn( 'rebuild', `${count} ${noun} install scripts not yet covered by allowScripts. ` + diff --git a/deps/npm/lib/commands/token.js b/deps/npm/lib/commands/token.js index b248d6a789a1f8..8f54e9d8725dfb 100644 --- a/deps/npm/lib/commands/token.js +++ b/deps/npm/lib/commands/token.js @@ -73,17 +73,22 @@ class Token extends BaseCommand { const parseable = this.npm.config.get('parseable') log.info('token', 'getting list') const tokens = await paginate('/-/npm/v1/tokens', this.npm.flatOptions) + + this.generateTokenIds(tokens, 6) + if (json) { output.buffer(tokens) return } if (parseable) { - output.standard(['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t')) + output.standard(['key', 'token', 'id', 'name', 'created', 'readonly', 'CIDR whitelist'].join('\t')) tokens.forEach(token => { output.standard( [ token.key, token.token, + token.id, + token.name, token.created, token.readonly ? 'true' : 'false', token.cidr_whitelist ? token.cidr_whitelist.join(',') : '', @@ -92,11 +97,10 @@ class Token extends BaseCommand { }) return } - this.generateTokenIds(tokens, 6) const chalk = this.npm.chalk for (const token of tokens) { const created = String(token.created).slice(0, 10) - output.standard(`${chalk.blue('Token')} ${token.token}… with id ${chalk.cyan(token.id)} created ${created}`) + output.standard(`${chalk.blue('Token')} ${token.token}… with id ${chalk.cyan(token.id)} name ${chalk.magenta(token.name)} created ${created}`) if (token.cidr_whitelist) { output.standard(`with IP whitelist: ${chalk.green(token.cidr_whitelist.join(','))}`) } diff --git a/deps/npm/lib/utils/allow-scripts-cmd.js b/deps/npm/lib/utils/allow-scripts-cmd.js index b6ec663124fe84..315b7b0b4fb9ce 100644 --- a/deps/npm/lib/utils/allow-scripts-cmd.js +++ b/deps/npm/lib/utils/allow-scripts-cmd.js @@ -3,6 +3,7 @@ const npa = require('npm-package-arg') const semver = require('semver') const pkgJson = require('@npmcli/package-json') const { trustedDisplay } = require('@npmcli/arborist/lib/script-allowed.js') +const getInstallScripts = require('@npmcli/arborist/lib/install-scripts.js') const checkAllowScripts = require('./check-allow-scripts.js') const resolveAllowScripts = require('./resolve-allow-scripts.js') const { @@ -10,6 +11,7 @@ const { applyDenyForPackage, nameKeyFor, } = require('./allow-scripts-writer.js') +const { classifyUnusedEntries } = require('./allow-scripts-prune.js') const BaseCommand = require('../base-cmd.js') // Parse a positional arg into a name and an optional version range. A bare @@ -34,8 +36,9 @@ const parsePositional = (arg) => { return { name, range: null } } -// Shared implementation for `npm approve-scripts` and `npm deny-scripts`. -// Subclasses set `verb` to `'approve'` or `'deny'`. +// Shared implementation for `npm approve-scripts`, `npm deny-scripts`, and the `npm install-scripts` namespace. +// `npm install-scripts` dispatches to `runMode('approve' | 'deny' | 'list', ...)`. +// The standalone commands set `static verb` and run through the default `exec`. // // Extends `BaseCommand` rather than `ArboristCmd` on purpose. Per RFC, // `allowScripts` is read from the workspace root's `package.json` only; @@ -48,13 +51,23 @@ class AllowScriptsCmd extends BaseCommand { static params = ['all', 'allow-scripts-pending', 'allow-scripts-pin', 'json'] static ignoreImplicitWorkspace = false - // Subclasses set `static verb = 'approve' | 'deny'`. + // Mode of the current run, set by runMode. + // One of 'approve', 'deny', 'list', or 'prune'. + #mode = null + + // verb drives the writers and summaries, which only run in the two write modes, so it is never read while listing. get verb () { - /* istanbul ignore next: every concrete subclass declares static verb */ - return this.constructor.verb + return this.#mode } + // Standalone `npm approve-scripts` / `npm deny-scripts` pick their mode from `static verb`. async exec (args) { + return this.runMode(this.constructor.verb, args) + } + + async runMode (mode, args) { + this.#mode = mode + if (this.npm.global) { throw Object.assign( new Error(`\`npm ${this.constructor.name}\` does not work for global installs`), @@ -62,19 +75,33 @@ class AllowScriptsCmd extends BaseCommand { ) } - const pending = !!this.npm.config.get('allow-scripts-pending') + // `prune` has its own flow: it reads the literal package.json#allowScripts + // map, not the resolved policy. + if (mode === 'prune') { + return this.runPrune(args) + } + + // `--allow-scripts-pending` is only honored by commands that declare it; the namespace lists via `ls` instead. + const pending = this.constructor.params.includes('allow-scripts-pending') && + !!this.npm.config.get('allow-scripts-pending') const all = !!this.npm.config.get('all') + // The `ls` subcommand lists, and so does `--allow-scripts-pending` on the write commands. + const list = mode === 'list' || pending - if (pending && (args.length > 0 || all)) { + if (list && (args.length > 0 || all)) { + const what = mode === 'list' ? '`npm install-scripts ls`' : '`--allow-scripts-pending`' throw this.usageError( - '`--allow-scripts-pending` cannot be combined with positional arguments or `--all`.' + `${what} cannot be combined with positional arguments or \`--all\`.` ) } - if (!pending && !all && args.length === 0) { + if (!list && !all && args.length === 0) { throw this.usageError() } - if (this.verb === 'deny' && pending) { - throw this.usageError('`npm deny-scripts --allow-scripts-pending` is not supported.') + if (mode === 'deny' && pending) { + throw this.usageError( + '`npm deny-scripts --allow-scripts-pending` is not supported; ' + + 'run `npm install-scripts ls` to list unreviewed packages.' + ) } const Arborist = require('@npmcli/arborist') @@ -91,7 +118,7 @@ class AllowScriptsCmd extends BaseCommand { // only lists; nothing runs. const unreviewed = await checkAllowScripts({ arb, npm: this.npm, includeWhenIgnored: true }) - if (pending) { + if (list) { return this.runPending(unreviewed) } @@ -129,7 +156,8 @@ class AllowScriptsCmd extends BaseCommand { } output.standard('') output.standard( - 'Run `npm approve-scripts ` to allow, or `npm deny-scripts ` to deny.' + 'Run `npm install-scripts approve ` to allow, ' + + 'or `npm install-scripts deny ` to deny.' ) } @@ -298,6 +326,78 @@ class AllowScriptsCmd extends BaseCommand { output.standard(`Nothing to ${this.verb}; allowScripts unchanged.`) } } + + // `npm install-scripts prune`: drop package.json#allowScripts entries that no + // longer match an installed package with an install script. Edits only + // package.json (never `.npmrc`/CLI policy); `--dry-run` reports without writing. + async runPrune (args) { + const all = !!this.npm.config.get('all') + if (args.length > 0 || all) { + throw this.usageError( + '`npm install-scripts prune` cannot be combined with positional arguments or `--all`.' + ) + } + + const dryRun = !!this.npm.config.get('dry-run') + const pkg = await pkgJson.load(this.npm.prefix) + const existing = pkg.content.allowScripts && typeof pkg.content.allowScripts === 'object' + ? pkg.content.allowScripts + : {} + + let removed = [] + if (Object.keys(existing).length > 0) { + const Arborist = require('@npmcli/arborist') + const arb = new Arborist({ + ...this.npm.flatOptions, + path: this.npm.prefix, + }) + await arb.loadActual() + + // Candidate install nodes (mirrors collectUnreviewedScripts), tagged with + // whether each has install scripts so the classifier can tell "gone" from + // "no longer has scripts". + const nodes = [] + for (const node of arb.actualTree.inventory.values()) { + if (node.isProjectRoot || node.isWorkspace || node.isLink || node.inBundle || node.inert) { + continue + } + const scripts = await getInstallScripts(node) + nodes.push({ node, hasScripts: Object.keys(scripts).length > 0 }) + } + + const { remaining, removed: unused } = classifyUnusedEntries(existing, nodes) + removed = unused + + if (removed.length > 0 && !dryRun) { + // Drop the field entirely when nothing is left rather than leaving `{}`. + pkg.update({ + allowScripts: Object.keys(remaining).length > 0 ? remaining : undefined, + }) + await pkg.save() + } + } + + this.printPruneSummary({ removed, dryRun }) + } + + printPruneSummary ({ removed, dryRun }) { + if (this.npm.flatOptions.json) { + output.buffer({ allowScripts: { removed, dryRun } }) + return + } + if (removed.length === 0) { + output.standard('No unused allowScripts entries.') + return + } + const entry = removed.length === 1 ? 'entry' : 'entries' + output.standard( + `${dryRun ? 'Would remove' : 'Removed'} ${removed.length} unused allowScripts ${entry}:` + ) + for (const { key, reason } of removed) { + const text = reason === 'not-installed' ? 'package not installed' : 'no install scripts' + output.standard(` ${key} (${text})`) + } + } } module.exports = AllowScriptsCmd diff --git a/deps/npm/lib/utils/allow-scripts-prune.js b/deps/npm/lib/utils/allow-scripts-prune.js new file mode 100644 index 00000000000000..1111b45d9ca3fd --- /dev/null +++ b/deps/npm/lib/utils/allow-scripts-prune.js @@ -0,0 +1,47 @@ +const npa = require('npm-package-arg') +const { matches } = require('@npmcli/arborist/lib/script-allowed.js') + +// Pure classifier behind `npm install-scripts prune`. +// +// Splits the `package.json#allowScripts` map into entries to keep (`remaining`) +// and unused ones to drop (`removed`). `nodes` is the install candidates as +// `{ node, hasScripts }`; the caller gathers them so this stays sync. +// +// An entry is unused (version-aware, by trusted identity) when: +// - no installed node matches the key -> reason 'not-installed' +// - it matches but none have scripts -> reason 'no-scripts' +// +// Unparseable keys are kept (prune never drops what it can't parse). Both +// `true` (approve) and `false` (deny) entries are pruned. +const classifyUnusedEntries = (allowScripts, nodes) => { + const remaining = {} + const removed = [] + + for (const [key, value] of Object.entries(allowScripts || {})) { + let parseable = true + try { + npa(key) + } catch { + parseable = false + } + if (!parseable) { + remaining[key] = value + continue + } + + const matching = nodes.filter(({ node }) => matches(node, key)) + if (matching.length === 0) { + removed.push({ key, value, reason: 'not-installed' }) + continue + } + if (!matching.some(({ hasScripts }) => hasScripts)) { + removed.push({ key, value, reason: 'no-scripts' }) + continue + } + remaining[key] = value + } + + return { remaining, removed } +} + +module.exports = { classifyUnusedEntries } diff --git a/deps/npm/lib/utils/allow-scripts-writer.js b/deps/npm/lib/utils/allow-scripts-writer.js index 310e22412a4a44..b9476905d427b0 100644 --- a/deps/npm/lib/utils/allow-scripts-writer.js +++ b/deps/npm/lib/utils/allow-scripts-writer.js @@ -108,15 +108,15 @@ const isSingleVersionPin = (key) => { // 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. +// the user can either widen it via `npm install-scripts deny ` 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}\` ` + + return `${key} is a versioned deny; run \`npm install-scripts deny ${widenTarget}\` ` + `to widen the deny to all versions of ${widenTarget}, or remove the entry ` + `to approve ${subject}.` } @@ -250,7 +250,41 @@ const applyApprovalForPackage = (existing, nodes, { pin = true } = {}) => { // 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)) + const versionedKeys = nodes.map(versionedKeyFor) + const installedKeys = new Set(versionedKeys.filter(Boolean)) + + // A registry dep with no `resolved` URL in the lockfile has no trustable + // version (getTrustedRegistryIdentity won't trust the tarball's + // node.version), so versionedKeyFor returns null and a `pkg@x.y.z` pin can + // never match it (npm/cli#9558). When any installed version can't be + // pinned, approve the whole package by name and drop now-redundant pins. + if (name && versionedKeys.some(key => !key)) { + for (const key of Object.keys(allowScripts)) { + if ( + keyTargetsNode(key, sample) && + key !== name && + isSingleVersionPin(key) && + allowScripts[key] === true + ) { + delete allowScripts[key] + changes.push({ key, change: 'removed-pinned-allow' }) + } + } + if (allowScripts[name] !== true) { + allowScripts[name] = true + changes.push({ key: name, change: 'added' }) + } + return { + allowScripts, + changes, + warning: changes.length + ? `${name}: approved by name (all versions) because its ` + + `package-lock.json entry has no "resolved" URL, so npm can't pin a ` + + `specific version. Run \`npm install\` to refresh the lockfile and ` + + `enable pinning.` + : undefined, + } + } for (const key of Object.keys(allowScripts)) { if ( diff --git a/deps/npm/lib/utils/cmd-list.js b/deps/npm/lib/utils/cmd-list.js index 1909df0d045469..8816d8294716ef 100644 --- a/deps/npm/lib/utils/cmd-list.js +++ b/deps/npm/lib/utils/cmd-list.js @@ -31,6 +31,7 @@ const commands = [ 'init', 'install', 'install-ci-test', + 'install-scripts', 'install-test', 'link', 'll', diff --git a/deps/npm/lib/utils/reify-output.js b/deps/npm/lib/utils/reify-output.js index 1ccbd4d0cad71a..6e25f94340632a 100644 --- a/deps/npm/lib/utils/reify-output.js +++ b/deps/npm/lib/utils/reify-output.js @@ -66,7 +66,8 @@ const reifyOutput = (npm, arb, extras = {}) => { if (showDiff) { output.standard(`${chalk.green('add')} ${d.ideal.name} ${d.ideal.package.version}`) } - if (actualTree.inventory.has(d.ideal)) { + // Linked store packages live under .store, absent from the logical actualTree, so identity lookup misses them; count each store package node (non-link). + if (actualTree.inventory.has(d.ideal) || (d.ideal.isInStore && !d.ideal.isLink)) { summary.added++ summary.add.push({ name: d.ideal.name, @@ -268,7 +269,7 @@ const unreviewedScriptsMessage = (npm, unreviewedScripts) => { ) } -// `npm approve-scripts` writes to a project package.json, which doesn't +// `npm install-scripts` writes to a project package.json, which doesn't // exist for global installs (it throws EGLOBAL). For those, point users at // the mechanism that does work globally: the `--allow-scripts` flag for a // one-off, or `npm config set allow-scripts` to persist it. @@ -282,8 +283,8 @@ const remediationLines = (npm, names) => { ] } return [ - 'Run `npm approve-scripts --allow-scripts-pending` to review, ' + - 'or `npm approve-scripts ` to allow.', + 'Run `npm install-scripts ls` to review, ' + + 'or `npm install-scripts approve ` to allow.', ] } diff --git a/deps/npm/lib/utils/sbom-cyclonedx.js b/deps/npm/lib/utils/sbom-cyclonedx.js index fe368e968baaa7..bd741718bef0d6 100644 --- a/deps/npm/lib/utils/sbom-cyclonedx.js +++ b/deps/npm/lib/utils/sbom-cyclonedx.js @@ -76,7 +76,7 @@ const toCyclonedxItem = (node, { packageType }) => { // Calculate purl from package spec let spec = npa(node.pkgid) spec = (spec.type === 'alias') ? spec.subSpec : spec - const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${node.resolved}` : '') + const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${encodeURIComponent(node.resolved)}` : '') if (node.package) { const toNormalize = new PackageJson() diff --git a/deps/npm/lib/utils/sbom-spdx.js b/deps/npm/lib/utils/sbom-spdx.js index 8ea75c688bc862..88797fd03effba 100644 --- a/deps/npm/lib/utils/sbom-spdx.js +++ b/deps/npm/lib/utils/sbom-spdx.js @@ -109,7 +109,7 @@ const toSpdxItem = (node, { packageType }) => { // Calculate purl from package spec let spec = npa(node.pkgid) spec = (spec.type === 'alias') ? spec.subSpec : spec - const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${node.resolved}` : '') + const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${encodeURIComponent(node.resolved)}` : '') /* For workspace nodes, use the location from their linkNode */ let location = node.location diff --git a/deps/npm/lib/utils/strict-allow-scripts-preflight.js b/deps/npm/lib/utils/strict-allow-scripts-preflight.js index d3575289fa8ed6..0c500018184c2e 100644 --- a/deps/npm/lib/utils/strict-allow-scripts-preflight.js +++ b/deps/npm/lib/utils/strict-allow-scripts-preflight.js @@ -48,19 +48,19 @@ const strictAllowScriptsPreflight = async ({ arb, npm, idealTreeOpts }) => { return ` ${label} (${events})` }).join('\n') - // `npm approve-scripts` / `npm deny-scripts` write to a project - // package.json, which doesn't exist for global installs. Point global - // users at the `--allow-scripts` flag and `npm config set allow-scripts`, - // which both work for global installs. Use the trusted display identity - // so the suggested `npm config set` value matches what the policy matches - // on, not the tarball's self-reported name. + // `npm install-scripts` writes to a project package.json, which doesn't + // exist for global installs. Point global users at the `--allow-scripts` + // flag and `npm config set allow-scripts`, which both work for global + // installs. Use the trusted display identity so the suggested `npm config + // set` value matches what the policy matches on, not the tarball's + // self-reported name. const names = unreviewed.map(({ node }) => trustedDisplay(node).name) const remediation = npm.global ? 'Allow them with `--allow-scripts`, persist them with ' + `\`${configSetAllowScripts(names)}\`, or bypass this ` + 'check with `--dangerously-allow-all-scripts`.' - : 'Approve them with `npm approve-scripts`, deny them with ' + - '`npm deny-scripts`, or bypass this check with ' + + : 'Approve them with `npm install-scripts approve`, deny them with ' + + '`npm install-scripts deny`, or bypass this check with ' + '`--dangerously-allow-all-scripts`.' throw Object.assign( diff --git a/deps/npm/man/man1/npm-access.1 b/deps/npm/man/man1/npm-access.1 index 5ffc6914ad0633..707bc69f167c40 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-ACCESS" "1" "June 2026" "NPM@11.18.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 83f72e33008086..091bcf2e3fe833 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-ADDUSER" "1" "June 2026" "NPM@11.18.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 index 1fa1807f197223..cd2aee3190b791 100644 --- a/deps/npm/man/man1/npm-approve-scripts.1 +++ b/deps/npm/man/man1/npm-approve-scripts.1 @@ -1,4 +1,4 @@ -.TH "NPM-APPROVE-SCRIPTS" "1" "June 2026" "NPM@11.17.0" "" +.TH "NPM-APPROVE-SCRIPTS" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-approve-scripts\fR - Approve install scripts for specific dependencies .SS "Synopsis" @@ -37,6 +37,8 @@ npm approve-scripts --allow-scripts-pending \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. +.P +If a registry dependency has no \fBresolved\fR URL in your \fBpackage-lock.json\fR (for example, an older lockfile or one written with \fBomit-lockfile-registry-resolved\fR), npm cannot verify a trusted version for it and cannot pin it: a \fBpkg@1.2.3\fR entry never matches, so the package keeps appearing under \fB--allow-scripts-pending\fR. \fBapprove-scripts\fR approves these by name (\fBpkg: true\fR) and warns when it does. To restore pinning, refresh the lockfile with \fBnpm install\fR. .SS "Examples" .P .RS 2 diff --git a/deps/npm/man/man1/npm-audit.1 b/deps/npm/man/man1/npm-audit.1 index 9dca2a16806cd8..4c9bf13ecc27ba 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-AUDIT" "1" "June 2026" "NPM@11.18.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 4e9fb5d6d1ddf7..529f369d12dac4 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-BUGS" "1" "June 2026" "NPM@11.18.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 056647fd1f366c..1f609ffd785afe 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-CACHE" "1" "June 2026" "NPM@11.18.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 88d49e4cfd29a5..2e62f4a1e73fcb 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-CI" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-ci\fR - Clean install a project .SS "Synopsis" @@ -75,7 +75,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 @@ -242,6 +244,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .SS "\fBdangerously-allow-all-scripts\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 15444b319c7642..5327001ec3f12e 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-COMPLETION" "1" "June 2026" "NPM@11.18.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 446486b119387b..82fd108f72c0c7 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-CONFIG" "1" "June 2026" "NPM@11.18.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 c370607b6fc112..68e369035eae76 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-DEDUPE" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-dedupe\fR - Reduce duplication in the package tree .SS "Synopsis" @@ -74,7 +74,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-deny-scripts.1 b/deps/npm/man/man1/npm-deny-scripts.1 index f60c26b5a3c410..67daaa87b720ee 100644 --- a/deps/npm/man/man1/npm-deny-scripts.1 +++ b/deps/npm/man/man1/npm-deny-scripts.1 @@ -1,4 +1,4 @@ -.TH "NPM-DENY-SCRIPTS" "1" "June 2026" "NPM@11.17.0" "" +.TH "NPM-DENY-SCRIPTS" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-deny-scripts\fR - Deny install scripts for specific dependencies .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-deprecate.1 b/deps/npm/man/man1/npm-deprecate.1 index e04d1228f86db9..1d89f067e7305e 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-DEPRECATE" "1" "June 2026" "NPM@11.18.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 ed49280aa5a1e9..cc06fc95438658 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-DIFF" "1" "June 2026" "NPM@11.18.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 3aeacd51dccecc..6c3e74e3c6513d 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-DIST-TAG" "1" "June 2026" "NPM@11.18.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 e3e9a43aac9cf8..952c18cf115f69 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-DOCS" "1" "June 2026" "NPM@11.18.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 1e89fec2f3f986..1e8813a13a1488 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-DOCTOR" "1" "June 2026" "NPM@11.18.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 530543a029b114..d46ea046ddb046 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-EDIT" "1" "June 2026" "NPM@11.18.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 3122968e406543..93245000380fb7 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-EXEC" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-exec\fR - Run a command from a local or remote npm package .SS "Synopsis" @@ -193,6 +193,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .SS "\fBdangerously-allow-all-scripts\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-explain.1 b/deps/npm/man/man1/npm-explain.1 index ad4b6a15bf64ce..0c4b22410bf64a 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-EXPLAIN" "1" "June 2026" "NPM@11.18.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 ee12137b4d6270..bd6788e463ea42 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-EXPLORE" "1" "June 2026" "NPM@11.18.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 287971553cc917..8cab256a973b0f 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-FIND-DUPES" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-find-dupes\fR - Find duplication in the package tree .SS "Synopsis" @@ -21,7 +21,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-fund.1 b/deps/npm/man/man1/npm-fund.1 index fbe8cdc9bee951..a334c59e990078 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-FUND" "1" "June 2026" "NPM@11.18.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 03c66b0379ffdd..1eb86d69f161e0 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-GET" "1" "June 2026" "NPM@11.18.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 00a4a24dce4a23..b1752ad2b563be 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-HELP-SEARCH" "1" "June 2026" "NPM@11.18.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 7462b626b6f017..aef5394daaa26a 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-HELP" "1" "June 2026" "NPM@11.18.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 80e94e4140e090..990e0a58ed2820 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-INIT" "1" "June 2026" "NPM@11.18.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 77d75105800b89..88db36a8799a22 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-INSTALL-CI-TEST" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-install-ci-test\fR - Install a project with a clean slate and run tests .SS "Synopsis" @@ -23,7 +23,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 @@ -190,6 +192,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .SS "\fBdangerously-allow-all-scripts\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-install-scripts.1 b/deps/npm/man/man1/npm-install-scripts.1 new file mode 100644 index 00000000000000..457b55c9fb7a8b --- /dev/null +++ b/deps/npm/man/man1/npm-install-scripts.1 @@ -0,0 +1,133 @@ +.TH "NPM-INSTALL-SCRIPTS" "1" "June 2026" "NPM@11.18.0" "" +.SH "NAME" +\fBnpm-install-scripts\fR - Manage install-script approvals for dependencies +.SS "Synopsis" +.P +.RS 2 +.nf +npm install-scripts approve \[lB] ...\[rB] +npm install-scripts approve --all +npm install-scripts deny \[lB] ...\[rB] +npm install-scripts deny --all +npm install-scripts ls +npm install-scripts prune +.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 is the recommended way to maintain that field. +.P +Dependency install scripts are blocked by default. Install commands silently skip lifecycle scripts for any dependency that does not have a matching entry in \fBallowScripts\fR, and end with a list of the packages whose scripts were skipped so you can review them here. +.P +This command only works inside a project that has a \fBpackage.json\fR. Running it with \fB--global\fR (\fB-g\fR) fails with an \fBEGLOBAL\fR error, since global installs (\fBnpm install -g\fR) and one-off executions (\fBnpm exec\fR / \fBnpx\fR) have no project \fBpackage.json\fR to write to. To allow install scripts in those contexts, use the \fB--allow-scripts\fR flag at install time (for example \fBnpm install -g --allow-scripts=canvas,sharp\fR) or persist the setting with \fBnpm config set allow-scripts=canvas,sharp --location=user\fR. +.P +There are four subcommands: +.P +.RS 2 +.nf +npm install-scripts approve \[lB] ...\[rB] +npm install-scripts approve --all +npm install-scripts deny \[lB] ...\[rB] +npm install-scripts deny --all +npm install-scripts ls +npm install-scripts prune +.fi +.RE +.P +\fBapprove\fR allows install scripts for the named packages. \fB\fR matches every installed version of that package. By default it 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. \fB--all\fR approves every package with unreviewed install scripts in one go. +.P +\fBdeny\fR records an explicit denial for the named packages (a name-only \fBfalse\fR entry), which survives \fBnpm install-scripts approve --all\fR and excludes the package from any future blanket approval. \fB--all\fR denies every package with unreviewed install scripts. +.P +\fBls\fR is read-only: it lists every package whose install scripts are not yet covered by \fBallowScripts\fR, without modifying \fBpackage.json\fR. +.P +\fBprune\fR removes \fBallowScripts\fR entries that no longer match an installed package with an install script, either because the package is no longer installed (a transitive dependency changed, or a pinned \fBpkg@1.2.3\fR was upgraded) or because it no longer has an install script. Both approvals (\fBtrue\fR) and denials (\fBfalse\fR) are removed. It edits only the \fBallowScripts\fR field in \fBpackage.json\fR, never \fB.npmrc\fR or \fB--allow-scripts\fR. Pass \fB--dry-run\fR to preview without writing. Unparseable keys are left alone. +.P +\fBapprove\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\fR will not silently re-allow a package you previously denied. +.P +The standalone commands npm help approve-scripts and npm help deny-scripts are aliases for \fBnpm install-scripts approve\fR and \fBnpm install-scripts deny\fR. +.SS "Examples" +.P +.RS 2 +.nf +# Approve all currently-installed install scripts after reviewing them +npm install-scripts approve --all + +# Approve specific packages, pinned to their installed version +npm install-scripts approve canvas sharp + +# Deny a package so it stays blocked +npm install-scripts deny telemetry-pkg + +# Preview which packages still need review +npm install-scripts ls + +# Preview stale allowScripts entries, then remove them +npm install-scripts prune --dry-run +npm install-scripts prune +.fi +.RE +.SS "Configuration" +.SS "\fBall\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Show or act on all packages, not just the ones your project directly depends on. For \fBnpm outdated\fR and \fBnpm ls\fR this lists every outdated or installed package. For \fBnpm approve-scripts\fR and \fBnpm deny-scripts\fR it selects every package with pending install scripts. +.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 "\fBdry-run\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +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. +.P +Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. +.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 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-install-test.1 b/deps/npm/man/man1/npm-install-test.1 index bea21d8f9996f4..56a0a454c8e601 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-INSTALL-TEST" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-install-test\fR - Install package(s) and run tests .SS "Synopsis" @@ -66,7 +66,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 @@ -267,6 +269,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .SS "\fBdangerously-allow-all-scripts\fR" .RS 0 .IP \(bu 4 @@ -302,6 +306,9 @@ If the requested version is a \fBdist-tag\fR and the given tag does not pass the .P If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .P +As with \fBmin-release-age\fR, when this cutoff blocks a fix that \fBnpm audit +fix\fR would install, npm keeps the vulnerable version, warns, and exits with a non-zero code. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .SS "\fBmin-release-age\fR" .RS 0 @@ -316,6 +323,8 @@ If set, npm will build the npm tree such that only versions that were available .P This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P +When this window stops \fBnpm audit fix\fR from installing a patched version (because the fix was published too recently), npm keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code. To install the fix, add the package to \fBmin-release-age-exclude\fR, or relax \fBmin-release-age\fR or \fBbefore\fR. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .P This value is not exported to the environment for child processes. diff --git a/deps/npm/man/man1/npm-install.1 b/deps/npm/man/man1/npm-install.1 index c4a9d33e00e736..1c853a06d9e7a1 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-INSTALL" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-install\fR - Install a package .SS "Synopsis" @@ -456,7 +456,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 @@ -657,6 +659,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .SS "\fBdangerously-allow-all-scripts\fR" .RS 0 .IP \(bu 4 @@ -692,6 +696,9 @@ If the requested version is a \fBdist-tag\fR and the given tag does not pass the .P If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .P +As with \fBmin-release-age\fR, when this cutoff blocks a fix that \fBnpm audit +fix\fR would install, npm keeps the vulnerable version, warns, and exits with a non-zero code. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .SS "\fBmin-release-age\fR" .RS 0 @@ -706,6 +713,8 @@ If set, npm will build the npm tree such that only versions that were available .P This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P +When this window stops \fBnpm audit fix\fR from installing a patched version (because the fix was published too recently), npm keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code. To install the fix, add the package to \fBmin-release-age-exclude\fR, or relax \fBmin-release-age\fR or \fBbefore\fR. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .P This value is not exported to the environment for child processes. diff --git a/deps/npm/man/man1/npm-link.1 b/deps/npm/man/man1/npm-link.1 index 52b718e15e9cab..9b45424b521c13 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-LINK" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-link\fR - Symlink a package folder .SS "Synopsis" @@ -133,7 +133,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-ll.1 b/deps/npm/man/man1/npm-ll.1 index cd62068571d750..76cca43a86c547 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-LL" "1" "June 2026" "NPM@11.18.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 3848cf175db3cf..c08365bf41b291 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-LOGIN" "1" "June 2026" "NPM@11.18.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 ebb1c62f319920..e27692a1f361d6 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-LOGOUT" "1" "June 2026" "NPM@11.18.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 f1d982134141bd..8d6a48be7adb13 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-LS" "1" "June 2026" "NPM@11.18.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.17.0 /path/to/npm +npm@11.18.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 879be21b203a8f..87b14daf94bb7e 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-ORG" "1" "June 2026" "NPM@11.18.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 446ddba893f360..0b2677ecbe64e4 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-OUTDATED" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-outdated\fR - Check for outdated packages .SS "Synopsis" @@ -182,6 +182,9 @@ If the requested version is a \fBdist-tag\fR and the given tag does not pass the .P If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .P +As with \fBmin-release-age\fR, when this cutoff blocks a fix that \fBnpm audit +fix\fR would install, npm keeps the vulnerable version, warns, and exits with a non-zero code. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .SS "\fBmin-release-age\fR" .RS 0 @@ -196,6 +199,8 @@ If set, npm will build the npm tree such that only versions that were available .P This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P +When this window stops \fBnpm audit fix\fR from installing a patched version (because the fix was published too recently), npm keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code. To install the fix, add the package to \fBmin-release-age-exclude\fR, or relax \fBmin-release-age\fR or \fBbefore\fR. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .P This value is not exported to the environment for child processes. diff --git a/deps/npm/man/man1/npm-owner.1 b/deps/npm/man/man1/npm-owner.1 index f257550482ccad..b4239474547451 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-OWNER" "1" "June 2026" "NPM@11.18.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 64de415452732e..4220d6eab1ba48 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-PACK" "1" "June 2026" "NPM@11.18.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 8f4292b8b5e9a2..b214ef9bbe299f 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-PING" "1" "June 2026" "NPM@11.18.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 fba0a9b61aee9e..e61280ea9b3b51 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-PKG" "1" "June 2026" "NPM@11.18.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 17248aa54d5396..8e5f186bc4940d 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-PREFIX" "1" "June 2026" "NPM@11.18.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 c050f580f4c853..7a3a7c3e73aed3 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-PROFILE" "1" "June 2026" "NPM@11.18.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 d70133afd0dcfc..d36938d3e27db9 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-PRUNE" "1" "June 2026" "NPM@11.18.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 d25d25c7590460..36ad2d781aca22 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-PUBLISH" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-publish\fR - Publish a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-query.1 b/deps/npm/man/man1/npm-query.1 index 654f2c33242b56..f5e07ec1954db5 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-QUERY" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-query\fR - Dependency selector query .SS "Synopsis" @@ -289,6 +289,9 @@ If the requested version is a \fBdist-tag\fR and the given tag does not pass the .P If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .P +As with \fBmin-release-age\fR, when this cutoff blocks a fix that \fBnpm audit +fix\fR would install, npm keeps the vulnerable version, warns, and exits with a non-zero code. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .SS "\fBmin-release-age\fR" .RS 0 @@ -303,6 +306,8 @@ If set, npm will build the npm tree such that only versions that were available .P This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P +When this window stops \fBnpm audit fix\fR from installing a patched version (because the fix was published too recently), npm keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code. To install the fix, add the package to \fBmin-release-age-exclude\fR, or relax \fBmin-release-age\fR or \fBbefore\fR. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .P This value is not exported to the environment for child processes. diff --git a/deps/npm/man/man1/npm-rebuild.1 b/deps/npm/man/man1/npm-rebuild.1 index 9c4aac4a249191..972bb0fadefa4c 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-REBUILD" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-rebuild\fR - Rebuild a package .SS "Synopsis" @@ -127,6 +127,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .SS "\fBdangerously-allow-all-scripts\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 2ddf9937d8e715..f354706b7a96bc 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-REPO" "1" "June 2026" "NPM@11.18.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 2b9b1161058b44..754c1450517e85 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-RESTART" "1" "June 2026" "NPM@11.18.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 7b85912e45cfb0..21a04731a3c84f 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-ROOT" "1" "June 2026" "NPM@11.18.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 0beb0b5fd04437..c481d3ea7f3a57 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-RUN" "1" "June 2026" "NPM@11.18.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 c04aca73069b15..4528c2287d3689 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-SBOM" "1" "June 2026" "NPM@11.18.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 72e8175649c820..88088ac8303093 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-SEARCH" "1" "June 2026" "NPM@11.18.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 84190092eaa846..feee9c27be677c 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-SET" "1" "June 2026" "NPM@11.18.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 faf1d8081df6a9..12d1b01f5553f4 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-SHRINKWRAP" "1" "June 2026" "NPM@11.18.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 20549ea1b36821..9e14f2c5b25179 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-STAGE" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-stage\fR - Stage packages for publishing .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-star.1 b/deps/npm/man/man1/npm-star.1 index 545945e0bdbc12..37d94b17a7c376 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-STAR" "1" "June 2026" "NPM@11.18.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 1eb8ca9b8f31ab..4db22ed60395ec 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-STARS" "1" "June 2026" "NPM@11.18.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 7722f5aa5bfab9..0729d9cd175465 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-START" "1" "June 2026" "NPM@11.18.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 df8ec8448baad3..e9261943c03c6e 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-STOP" "1" "June 2026" "NPM@11.18.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 4ef28794fc6c2d..d1e9e481cd882f 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-TEAM" "1" "June 2026" "NPM@11.18.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 b000e1bc6417fb..a9c28e79e980e5 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-TEST" "1" "June 2026" "NPM@11.18.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 a6823c2e2fa530..383a825d14fbde 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-TOKEN" "1" "June 2026" "NPM@11.18.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 9d20c2d45e8e59..5dc2dfc702a25f 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-TRUST" "1" "June 2026" "NPM@11.18.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 896fdb1f228fd7..2fc8e0050091b9 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-UNDEPRECATE" "1" "June 2026" "NPM@11.18.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 7384f0c4b2ac20..d2843a8a330506 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-UNINSTALL" "1" "June 2026" "NPM@11.18.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 15ece4ea1512d5..7c62b0c1324a31 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-UNPUBLISH" "1" "June 2026" "NPM@11.18.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 756d74ff719167..fe080180b661e7 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-UNSTAR" "1" "June 2026" "NPM@11.18.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 1d2904e2000df9..5ad8146b0da980 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-UPDATE" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-update\fR - Update packages .SS "Synopsis" @@ -174,7 +174,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBlegacy-bundling\fR" .RS 0 .IP \(bu 4 @@ -303,6 +305,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .SS "\fBdangerously-allow-all-scripts\fR" .RS 0 .IP \(bu 4 @@ -338,6 +342,9 @@ If the requested version is a \fBdist-tag\fR and the given tag does not pass the .P If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .P +As with \fBmin-release-age\fR, when this cutoff blocks a fix that \fBnpm audit +fix\fR would install, npm keeps the vulnerable version, warns, and exits with a non-zero code. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .SS "\fBmin-release-age\fR" .RS 0 @@ -352,6 +359,8 @@ If set, npm will build the npm tree such that only versions that were available .P This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P +When this window stops \fBnpm audit fix\fR from installing a patched version (because the fix was published too recently), npm keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code. To install the fix, add the package to \fBmin-release-age-exclude\fR, or relax \fBmin-release-age\fR or \fBbefore\fR. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .P This value is not exported to the environment for child processes. diff --git a/deps/npm/man/man1/npm-version.1 b/deps/npm/man/man1/npm-version.1 index 165689083e7fd8..d80b0e8288c096 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-VERSION" "1" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBnpm-version\fR - Bump a package version .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-view.1 b/deps/npm/man/man1/npm-view.1 index f487a05cc30e92..1c965ded858613 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-VIEW" "1" "June 2026" "NPM@11.18.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 93c147ae162664..7ddb5ca6a84e76 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-WHOAMI" "1" "June 2026" "NPM@11.18.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 29634dd59b29b6..58f6815ebadf92 100644 --- a/deps/npm/man/man1/npm.1 +++ b/deps/npm/man/man1/npm.1 @@ -1,4 +1,4 @@ -.TH "NPM" "1" "June 2026" "NPM@11.17.0" "" +.TH "NPM" "1" "June 2026" "NPM@11.18.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.17.0 +11.18.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 d8d510d6d9bf09..c3a4f521f66762 100644 --- a/deps/npm/man/man1/npx.1 +++ b/deps/npm/man/man1/npx.1 @@ -1,4 +1,4 @@ -.TH "NPX" "1" "June 2026" "NPM@11.17.0" "" +.TH "NPX" "1" "June 2026" "NPM@11.18.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 5dd5cc2f5c537f..0202a97198c8f9 100644 --- a/deps/npm/man/man5/folders.5 +++ b/deps/npm/man/man5/folders.5 @@ -1,4 +1,4 @@ -.TH "FOLDERS" "5" "June 2026" "NPM@11.17.0" "" +.TH "FOLDERS" "5" "June 2026" "NPM@11.18.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 ecd657bf2a4ba8..8e23936cba2da1 100644 --- a/deps/npm/man/man5/install.5 +++ b/deps/npm/man/man5/install.5 @@ -1,4 +1,4 @@ -.TH "INSTALL" "5" "June 2026" "NPM@11.17.0" "" +.TH "INSTALL" "5" "June 2026" "NPM@11.18.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 5dd5cc2f5c537f..0202a97198c8f9 100644 --- a/deps/npm/man/man5/npm-global.5 +++ b/deps/npm/man/man5/npm-global.5 @@ -1,4 +1,4 @@ -.TH "FOLDERS" "5" "June 2026" "NPM@11.17.0" "" +.TH "FOLDERS" "5" "June 2026" "NPM@11.18.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 b1a7646edc8f3b..bd9d1bd5b7ee94 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" "June 2026" "NPM@11.17.0" "" +.TH "PACKAGE.JSON" "5" "June 2026" "NPM@11.18.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 25c446d6238919..5f1f80576e9a09 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" "June 2026" "NPM@11.17.0" "" +.TH "NPM-SHRINKWRAP.JSON" "5" "June 2026" "NPM@11.18.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 f57ca1f46819c5..14d449b3111b7e 100644 --- a/deps/npm/man/man5/npmrc.5 +++ b/deps/npm/man/man5/npmrc.5 @@ -1,4 +1,4 @@ -.TH ".NPMRC" "5" "June 2026" "NPM@11.17.0" "" +.TH ".NPMRC" "5" "June 2026" "NPM@11.18.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 b1a7646edc8f3b..bd9d1bd5b7ee94 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" "June 2026" "NPM@11.17.0" "" +.TH "PACKAGE.JSON" "5" "June 2026" "NPM@11.18.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 527fc62ab1bb65..0223c95c68a4d5 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" "June 2026" "NPM@11.17.0" "" +.TH "PACKAGE-LOCK.JSON" "5" "June 2026" "NPM@11.18.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 75c854342d85ae..eb4145df952600 100644 --- a/deps/npm/man/man7/config.7 +++ b/deps/npm/man/man7/config.7 @@ -1,4 +1,4 @@ -.TH "CONFIG" "7" "June 2026" "NPM@11.17.0" "" +.TH "CONFIG" "7" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBConfig\fR - About npm configuration .SS "Description" @@ -333,6 +333,9 @@ If the requested version is a \fBdist-tag\fR and the given tag does not pass the .P If \fBbefore\fR and \fBmin-release-age\fR are both set in the same source, \fBbefore\fR wins (an explicit absolute date overrides a relative window). Across sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. .P +As with \fBmin-release-age\fR, when this cutoff blocks a fix that \fBnpm audit +fix\fR would install, npm keeps the vulnerable version, warns, and exits with a non-zero code. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .SS "\fBbin-links\fR" .RS 0 @@ -1020,7 +1023,9 @@ Type: "hoisted", "nested", "shallow", or "linked" .RE 0 .P -Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: (experimental) install in node_modules/.store, link in place, unhoisted. +Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +.P +We recommend that package authors use \fB--install-strategy=linked\fR during development to catch undeclared ("phantom") dependencies before publishing: the isolated layout only exposes a package's declared dependencies, so an \fBimport\fR of a package that was never added to \fBpackage.json\fR can fail instead of resolving by accident and shipping broken. See \fBCatching undeclared ("phantom") dependencies\fR \fI\(la/using-npm/developers#catching-undeclared-phantom-dependencies\(ra\fR. .SS "\fBjson\fR" .RS 0 .IP \(bu 4 @@ -1202,6 +1207,8 @@ If set, npm will build the npm tree such that only versions that were available .P This flag is a complement to \fBbefore\fR, which accepts an exact date instead of a relative number of days. The two may coexist (e.g. \fBmin-release-age\fR in your \fB.npmrc\fR is preserved when npm internally spawns a sub-process with \fB--before\fR while preparing a \fBgit:\fR or \fBgithub:\fR dependency); when both apply, \fBbefore\fR wins within a single source and across sources the standard precedence rules apply. .P +When this window stops \fBnpm audit fix\fR from installing a patched version (because the fix was published too recently), npm keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code. To install the fix, add the package to \fBmin-release-age-exclude\fR, or relax \fBmin-release-age\fR or \fBbefore\fR. +.P Packages whose names match \fBmin-release-age-exclude\fR are exempt from this filter. .P This value is not exported to the environment for child processes. @@ -1586,7 +1593,9 @@ Defines behavior for replacing the registry host in a lockfile with the configur .P The default behavior is to replace package dist URLs from the default registry (https://registry.npmjs.org) to the configured registry. If set to "never", then use the registry value. If set to "always", then replace the registry host with the configured host every time. .P -You may also specify a bare hostname (e.g., "registry.npmjs.org"). +You may also specify a bare hostname (e.g., "registry.npmjs.org") to only replace URLs coming from that host. +.P +You may also specify a full URL including a path (e.g., "https://old-registry.example.com/npm/path"). In that case, resolved URLs whose host and path begin with that prefix will have the entire prefix replaced with the configured registry URL (host and path), without duplicating path segments. .SS "\fBsave\fR" .RS 0 .IP \(bu 4 @@ -1847,6 +1856,8 @@ Type: Boolean 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. +.P +Optional dependencies that cannot be installed on the current platform or engine (a non-matching \fBos\fR, \fBcpu\fR, or \fBlibc\fR) are not flagged, because their install scripts never run. .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 be698295b15fae..0a9ea65ac2a4f9 100644 --- a/deps/npm/man/man7/dependency-selectors.7 +++ b/deps/npm/man/man7/dependency-selectors.7 @@ -1,4 +1,4 @@ -.TH "SELECTORS" "7" "June 2026" "NPM@11.17.0" "" +.TH "SELECTORS" "7" "June 2026" "NPM@11.18.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 0956c032d8f6c2..85cc7a5f794032 100644 --- a/deps/npm/man/man7/developers.7 +++ b/deps/npm/man/man7/developers.7 @@ -1,4 +1,4 @@ -.TH "DEVELOPERS" "7" "June 2026" "NPM@11.17.0" "" +.TH "DEVELOPERS" "7" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBDevelopers\fR - Developer guide .SS "Description" @@ -184,6 +184,23 @@ npm install ../my-package to install it locally into the node_modules folder in that other place. .P Then go into the node-repl, and try using require("my-thing") to bring in your module's main module. +.SS "Catching undeclared (\"phantom\") dependencies" +.P +Under the default hoisted \fBnode_modules\fR layout, your package can \fBimport\fR a dependency it never declared and still resolve it. A transitive dependency hoisted alongside it, or your workspace root's \fBnode_modules\fR, happens to satisfy the \fBimport\fR. That undeclared ("phantom") dependency passes your own build silently, then fails for anyone who installs your package on its own. +.P +We recommend developing your package under \fB\[rs]fBinstall-strategy=linked\[rs]fR\fR \fI\(la/using-npm/config#install-strategy\(ra\fR. The isolated layout only exposes a package's \fIdeclared\fR dependencies, so an \fBimport\fR of an undeclared package fails for you during development instead of resolving by accident, shipping broken, and failing for your users: +.P +.RS 2 +.nf +npm install --install-strategy=linked +npm test +.fi +.RE +.RS 0 +.P +\fBNote:\fR This doesn't catch every case. A dependency that's still satisfied at your build by a \fBdevDependency\fR or by your workspace root's \fBnode_modules\fR can resolve fine for you and still be missing for whoever installs your package. So treat it as one check, not a guarantee, alongside auditing the dependencies your published package actually uses. +.RE 0 + .SS "Create a User Account" .P Create a user with the adduser command. It works like this: diff --git a/deps/npm/man/man7/logging.7 b/deps/npm/man/man7/logging.7 index 3d037957e5566d..350348aa714eb8 100644 --- a/deps/npm/man/man7/logging.7 +++ b/deps/npm/man/man7/logging.7 @@ -1,4 +1,4 @@ -.TH "LOGGING" "7" "June 2026" "NPM@11.17.0" "" +.TH "LOGGING" "7" "June 2026" "NPM@11.18.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 3224946c994e53..bb6cad63c5113e 100644 --- a/deps/npm/man/man7/orgs.7 +++ b/deps/npm/man/man7/orgs.7 @@ -1,4 +1,4 @@ -.TH "ORGANIZATIONS" "7" "June 2026" "NPM@11.17.0" "" +.TH "ORGANIZATIONS" "7" "June 2026" "NPM@11.18.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 8c57645a9ca7fa..2ec37301d540ef 100644 --- a/deps/npm/man/man7/package-spec.7 +++ b/deps/npm/man/man7/package-spec.7 @@ -1,4 +1,4 @@ -.TH "SPEC" "7" "June 2026" "NPM@11.17.0" "" +.TH "SPEC" "7" "June 2026" "NPM@11.18.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 6fc1fa42503ec1..49f471fd2cc1e8 100644 --- a/deps/npm/man/man7/registry.7 +++ b/deps/npm/man/man7/registry.7 @@ -1,4 +1,4 @@ -.TH "REGISTRY" "7" "June 2026" "NPM@11.17.0" "" +.TH "REGISTRY" "7" "June 2026" "NPM@11.18.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 63295bda59554e..de875cb8e0c922 100644 --- a/deps/npm/man/man7/removal.7 +++ b/deps/npm/man/man7/removal.7 @@ -1,4 +1,4 @@ -.TH "REMOVAL" "7" "June 2026" "NPM@11.17.0" "" +.TH "REMOVAL" "7" "June 2026" "NPM@11.18.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 5d81873536a680..793031ca011065 100644 --- a/deps/npm/man/man7/scope.7 +++ b/deps/npm/man/man7/scope.7 @@ -1,4 +1,4 @@ -.TH "SCOPE" "7" "June 2026" "NPM@11.17.0" "" +.TH "SCOPE" "7" "June 2026" "NPM@11.18.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 6f104de691ad61..368e0fd18930fa 100644 --- a/deps/npm/man/man7/scripts.7 +++ b/deps/npm/man/man7/scripts.7 @@ -1,4 +1,4 @@ -.TH "SCRIPTS" "7" "June 2026" "NPM@11.17.0" "" +.TH "SCRIPTS" "7" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBScripts\fR - How npm handles the "scripts" field .SS "Description" diff --git a/deps/npm/man/man7/workspaces.7 b/deps/npm/man/man7/workspaces.7 index d68c1394c1c877..5dfa00e6492796 100644 --- a/deps/npm/man/man7/workspaces.7 +++ b/deps/npm/man/man7/workspaces.7 @@ -1,4 +1,4 @@ -.TH "WORKSPACES" "7" "June 2026" "NPM@11.17.0" "" +.TH "WORKSPACES" "7" "June 2026" "NPM@11.18.0" "" .SH "NAME" \fBWorkspaces\fR - Working with workspaces .SS "Description" diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js index 68527972b1fc39..63157b9a550de6 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js @@ -97,6 +97,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { #loadFailures = new Set() #manifests = new Map() #mutateTree = false + #requestedTreeMutation = false // a map of each module in a peer set to the thing that depended on // that set of peers in the first place. Use a WeakMap so that we // don't hold onto references for nodes that are garbage collected. @@ -261,12 +262,13 @@ module.exports = cls => class IdealTreeBuilder extends cls { // set if we add anything, but also set here if we know we'll make // changes and thus have to maybe prune later. - this.#mutateTree = !!( + this.#requestedTreeMutation = !!( options.add || options.rm || update.all || update.names.length ) + this.#mutateTree = this.#requestedTreeMutation } // load the initial tree, either the virtualTree from a shrinkwrap, @@ -344,7 +346,9 @@ module.exports = cls => class IdealTreeBuilder extends cls { filter: node => node, visit: node => { for (const edge of node.edgesOut.values()) { - const skipPeerOptional = edge.type === 'peerOptional' && this.options.save === false + const skipPeerOptional = edge.type === 'peerOptional' && + this.options.save === false && + !this.#requestedTreeMutation if (!skipPeerOptional && (!edge.to || !edge.valid)) { this.#depsQueue.push(node) break // no need to continue the loop after the first hit @@ -579,6 +583,17 @@ module.exports = cls => class IdealTreeBuilder extends cls { // and leaving the user subject to getting it overwritten later anyway. async #queueVulnDependents (options) { for (const vuln of this.auditReport.values()) { + // A fix is available in-range but a release-age window blocks the patched + // version, so audit fix leaves this package at a vulnerable version. + if (vuln.fixBlockedByReleaseAge) { + const { version, before } = vuln.fixBlockedByReleaseAge + const cutoff = new Date(before).toISOString().slice(0, 10) + log.warn('audit', `A fix for ${vuln.name} is available (${vuln.name}@${version}) ` + + `but was published after the configured release-age cutoff (${cutoff}), so ` + + `${vuln.name} was left at a vulnerable version.\n` + + `To install it, add "${vuln.name}" to min-release-age-exclude, or relax ` + + 'min-release-age or before.') + } for (const node of vuln.nodes) { const bundler = node.getBundler() @@ -1036,9 +1051,16 @@ This is a one-time fix-up, please be patient... } const { from, valid, peerConflicted } = edgeIn if (!peerConflicted && !valid) { - if (this.#depsSeen.has(from) && this.options.save) { - // Re-queue already-processed nodes when a newly placed dep creates an invalid edge during npm install (save=true). - // This handles the case where a peerOptional dep was valid (missing) when the node was first processed, but becomes invalid when the dep is later placed by another path with a version that doesn't satisfy the peer spec. + if (this.#depsSeen.has(from) && + (this.options.save || + (this.options.save === false && this.#requestedTreeMutation))) { + // Re-queue already-processed nodes when a newly placed dep + // creates an invalid edge during npm install or another + // lockfile-mutating operation. This handles the case where a + // peerOptional dep was valid (missing) when the node was + // first processed, but becomes invalid when the dep is later + // placed by another path with a version that doesn't satisfy + // the peer spec. // See npm/cli#8726. this.#depsSeen.delete(from) this.#depsQueue.push(from) @@ -1249,11 +1271,16 @@ This is a one-time fix-up, please be patient... continue } - // If the edge has an error, there's a problem, unless it's peerOptional and we're not saving (e.g. npm ci), in which case we trust the lockfile and skip re-resolution. - // When saving (npm install), peerOptional invalid edges ARE treated as problems so the lockfile gets fixed. + // If the edge has an error, there's a problem, unless it's peerOptional + // and we're not saving or otherwise mutating (e.g. npm ci), in which + // case we trust the lockfile and skip re-resolution. When saving (npm + // install) or updating, peerOptional invalid edges ARE treated as + // problems so the lockfile gets fixed. // See npm/cli#8726. if (!edge.valid) { - if (edge.type !== 'peerOptional' || this.options.save !== false) { + if (edge.type !== 'peerOptional' || + this.options.save !== false || + this.#requestedTreeMutation) { problems.push(edge) } continue @@ -1568,6 +1595,10 @@ This is a one-time fix-up, please be patient... !link.target.parent && !link.target.fsParent || unseenLink) { + // Forward the link's overrides before its subtree resolves, so a root override reaches a transitive dep across the link boundary (npm/cli#9659). + if (link.overrides) { + link.target.updateOverridesEdgeInAdded(link.overrides) + } this.addTracker('idealTree', link.target.name, link.target.location) this.#depsQueue.push(link.target) } 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 a63f607ef6877a..a07c55e3f283c6 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 @@ -266,7 +266,9 @@ module.exports = cls => class IsolatedReifier extends cls { } // local `file:` deps (non-workspace fsChildren) should be treated as local dependencies, not external, so they get symlinked directly instead of being extracted into the store. - const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n) + // A file: dep surfaces as a Link edge whose resolved spec starts with file:; detect it from the edge so the target is treated as local even when it is absent from idealTree.fsChildren (a workspace consumer, or a target outside the repo root via npm link). + const fileLinkTargets = new Set(edges.filter(e => e.to?.isLink && e.to.resolved?.startsWith('file:')).map(e => e.to.target)) + const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n) || fileLinkTargets.has(n) const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target) // Optional peers declared only in peerDependenciesMeta (e.g. `@types/react`) have no edge, so the materialization above misses them. diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/load-actual.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/load-actual.js index 2356227a5baa31..2158f6d32ae1aa 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/load-actual.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/load-actual.js @@ -1,6 +1,6 @@ // mix-in implementing the loadActual method -const { dirname, join, normalize, relative, resolve } = require('node:path') +const { dirname, join, normalize, relative, resolve, sep } = require('node:path') const PackageJson = require('@npmcli/package-json') const { readdirScoped } = require('@npmcli/fs') @@ -70,6 +70,7 @@ module.exports = cls => class ActualLoader extends cls { // only reset root flags if we're not re-rooting, // otherwise leave as-is calcDepFlags(tree, !options.root) + this.#repropagateOverrides() this.actualTree = treeCheck(tree) return this.actualTree }) @@ -141,6 +142,7 @@ module.exports = cls => class ActualLoader extends cls { root: this.#actualTree, }) await this[_setWorkspaces](this.#actualTree) + this.#linkActualWorkspaces() this.#transplant(root) return this.#actualTree @@ -173,6 +175,8 @@ module.exports = cls => class ActualLoader extends cls { await Promise.all(promises) } + this.#linkActualWorkspaces() + if (!ignoreMissing) { await this.#findMissingEdges() } @@ -211,6 +215,48 @@ module.exports = cls => class ActualLoader extends cls { return this.#actualTree } + // Under the linked (isolated) strategy, workspaces the root does not depend on are not symlinked into the root node_modules, so neither the on-disk scan nor the hidden lockfile gives the root's workspace edges a node_modules/ link to resolve to. + // Synthesize those links from the authoritative workspaces map so npm ls and npm query surface the workspaces, matching hoisted and the logical package-lock. + #linkActualWorkspaces () { + const root = this.#actualTree + if (this.options.installStrategy !== 'linked' || !root.workspaces) { + return + } + // Declared workspaces ARE symlinked at root node_modules under linked, so a missing link there is a real problem that must surface as UNMET. + // Only undeclared workspaces are intentionally absent from root node_modules and need a synthesized link so their root edges resolve. + const pkg = root.package + const declared = new Set(Object.keys(Object.assign({}, + pkg.dependencies, + pkg.devDependencies, + pkg.optionalDependencies, + pkg.peerDependencies + ))) + // index loaded workspace targets by both path and realpath, so a workspace reached through a symlinked dir still matches + const byLoc = new Map() + for (const node of root.fsChildren) { + byLoc.set(node.path, node) + byLoc.set(node.realpath, node) + } + for (const [name, path] of root.workspaces.entries()) { + // declared workspaces, and any name already a root child, resolve their edge without a synthesized link + if (declared.has(name) || root.children.has(name)) { + continue + } + const target = byLoc.get(path) + if (target) { + // eslint-disable-next-line no-new + new Link({ + name, + path: resolve(root.path, 'node_modules', name), + realpath: target.realpath, + target, + parent: root, + root, + }) + } + } + } + #transplant (root) { if (!root || root === this.#actualTree) { return @@ -263,6 +309,9 @@ module.exports = cls => class ActualLoader extends cls { parent, root, loadOverrides, + // A package physically located in the linked strategy's store is a transitive dependency, not a real tree top, so it must not load its devDependencies. + // Never flag the loaded root itself, even if its own path happens to sit under a .store directory. + isInStore: path !== this.path && real.includes(`${sep}node_modules${sep}.store${sep}`), } try { @@ -352,6 +401,18 @@ module.exports = cls => class ActualLoader extends cls { } } + // Re-forward overrides through links once all edges are resolved, since a Link may forward before its subtree resolves and miss a transitive match (npm/cli#9619, #9659). + #repropagateOverrides () { + if (!this.#actualTree.overrides) { + return + } + for (const node of this.#actualTree.inventory.values()) { + if (node.isLink && node.overrides) { + node.recalculateOutEdgesOverrides() + } + } + } + async #findMissingEdges () { // try to resolve any missing edges by walking up the directory tree, // checking for the package in each node_modules folder. stop at the @@ -368,9 +429,10 @@ module.exports = cls => class ActualLoader extends cls { const depPromises = [] for (const [name, edge] of node.edgesOut.entries()) { - const notMissing = !edge.missing && - !(edge.to && (edge.to.dummy || edge.to.parent !== node)) - if (notMissing) { + // An unresolved optional edge reports missing === false, so check the target directly. + // Otherwise an installed optional dep that lives only as a store sibling is never loaded. + const resolved = edge.to && !edge.to.dummy && edge.to.parent === node + if (resolved) { continue } 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 e70a2186c29713..a99d328f5d2947 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js @@ -11,7 +11,7 @@ const { depth: dfwalk } = require('treeverse') 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 { resolve, delimiter } = require('node:path') const { isScriptAllowed } = require('../script-allowed.js') const boolEnv = b => b ? '1' : '' @@ -199,10 +199,21 @@ module.exports = cls => class Builder extends cls { const { package: { bin, scripts = {} } } = node.target const { preinstall, install, postinstall, prepare } = scripts const tests = { bin, preinstall, install, postinstall, prepare } + // A denied package (allowScripts resolves to `false`) still gets its + // bins linked; only its lifecycle scripts are skipped, matching + // --ignore-scripts (npm/cli#9681). --dangerously-allow-all-scripts + // bypasses the gate. + const scriptsDenied = + !this.options.dangerouslyAllowAllScripts && + isScriptAllowed(node, this.options.allowScripts) === false for (const [key, has] of Object.entries(tests)) { - if (has) { - this.#queues[key].push(node) + if (!has) { + continue } + if (key !== 'bin' && scriptsDenied) { + continue + } + this.#queues[key].push(node) } } timeEnd() @@ -226,18 +237,6 @@ 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 && @@ -301,6 +300,7 @@ module.exports = cls => class Builder extends cls { await promiseCallLimit(queue.map(node => async () => { const { path, + name, integrity, resolved, optional, @@ -309,6 +309,7 @@ module.exports = cls => class Builder extends cls { devOptional, package: pkg, location, + isInStore, } = node.target // skip any that we know we'll be deleting @@ -330,6 +331,12 @@ module.exports = cls => class Builder extends cls { npm_package_dev_optional: boolEnv(devOptional && !dev && !optional), } + // In the linked strategy a store package's dependencies are symlinked siblings in its store node_modules. + // A separate bin invoked by the script (e.g. napi-postinstall) resolves modules from its own realpath in the store and cannot see those deps, so expose them via NODE_PATH. + if (isInStore) { + const storeNodeModules = resolve(path, ...name.split('/').map(() => '..')) + env.NODE_PATH = [storeNodeModules, process.env.NODE_PATH].filter(Boolean).join(delimiter) + } const runOpts = { event, path, 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 c49f352cef1404..db5171ff7367b3 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js @@ -11,7 +11,7 @@ const { callLimit: promiseCallLimit } = require('promise-call-limit') const { depth: dfwalk } = require('treeverse') const { dirname, resolve, relative, join, sep } = require('node:path') const { log, time } = require('proc-log') -const { existsSync } = require('node:fs') +const { existsSync, realpathSync } = require('node:fs') const { lstat, mkdir, readdir, readlink, rm, symlink } = require('node:fs/promises') const { moveFile } = require('@npmcli/fs') const { subset, intersects } = require('semver') @@ -26,7 +26,8 @@ const optionalSet = require('../optional-set.js') const relpath = require('../relpath.js') const retirePath = require('../retire-path.js') const treeCheck = require('../tree-check.js') -const { defaultLockfileVersion } = require('../shrinkwrap.js') +const Shrinkwrap = require('../shrinkwrap.js') +const { defaultLockfileVersion } = Shrinkwrap const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js') const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js') @@ -77,6 +78,9 @@ module.exports = cls => class Reifier extends cls { #sparseTreeDirs = new Set() #sparseTreeRoots = new Set() #linkedActualForDiff = null + // Under the linked strategy the audit runs against this non-isolated ideal tree. + // The isolated tree's inventory has no queryable indexes and its edges route through symlinks, so auditing it reports no vulnerabilities. + #linkedIdealForAudit = null constructor (options) { super(options) @@ -86,7 +90,9 @@ module.exports = cls => class Reifier extends cls { // public method async reify (options = {}) { - const linked = (options.installStrategy || this.options.installStrategy) === 'linked' + // Global installs are normalized to the shallow strategy in the constructor; honor that here so a per-call installStrategy:'linked' can't re-engage the unsupported linked path. + const linked = !this.options.global && + (options.installStrategy || this.options.installStrategy) === 'linked' if (this.options.packageLockOnly && this.options.global) { const er = new Error('cannot generate lockfile for global packages') @@ -112,32 +118,50 @@ module.exports = cls => class Reifier extends cls { await this[_loadTrees](options) const oldTree = this.idealTree + // Kept to serialize the hidden lockfile from the on-disk .store/symlink layout. + let isolatedTree = null if (linked) { // swap out the tree with the isolated tree // this is currently technical debt which will be resolved in a refactor // of Node/Link trees - log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.') this.idealTree = await this.createIsolatedTree() + isolatedTree = this.idealTree if (this.actualTree) { this.#linkedActualForDiff = this.#buildLinkedActualForDiff( this.idealTree, this.actualTree ) } + // Keep the non-isolated tree so the quick audit can run against it. + this.#linkedIdealForAudit = oldTree } - await this[_diffTrees]() - await this.#reifyPackages() - if (linked) { - // The sweep mutates node_modules on disk, so skip it for dry runs and lockfile-only installs (those modes also short-circuit #reifyPackages). - // The sweep itself scopes to in-filter workspaces when a filter is active, so it's safe to run for filtered installs too. - if (!this.options.dryRun && !this.options.packageLockOnly) { - await this.#cleanOrphanedStoreEntries() + try { + await this[_diffTrees]() + await this.#reifyPackages() + if (linked) { + // The sweep mutates node_modules on disk, so skip it for dry runs and lockfile-only installs (those modes also short-circuit #reifyPackages). + // The sweep itself scopes to in-filter workspaces when a filter is active, so it's safe to run for filtered installs too. + if (!this.options.dryRun && !this.options.packageLockOnly) { + await this.#cleanOrphanedStoreEntries() + } + } else if (!this.options.dryRun && !this.options.packageLockOnly) { + // The .store directory is exclusively a linked-strategy artifact, and load-actual ignores dot-directories, so the diff never sees it. + // Remove it here when switching away from linked so it does not linger under hoisted/nested. + // Only do this for a full-project install: a workspace-filtered or --workspaces=false install may leave out-of-scope workspaces with links still pointing into the store. + const filtered = this.options.workspaces.length > 0 || !this.options.workspacesEnabled + if (!filtered) { + await this.#removeStaleStoreDir() + } + } + } finally { + // Restore the non-isolated tree so the lockfile is preserved and a reused Arborist never sees the isolated tree, even if reify throws. + if (linked) { + this.idealTree = oldTree } - // swap back in the idealTree - // so that the lockfile is preserved - this.idealTree = oldTree + // The quick audit has captured its tree synchronously by now, so drop the stashed references even on throw. + this.#linkedIdealForAudit = null + this.#linkedActualForDiff = null } await this[_saveIdealTree](options) - this.#linkedActualForDiff = null // clean inert for (const node of this.idealTree.inventory.values()) { if (node.inert) { @@ -230,17 +254,24 @@ module.exports = cls => class Reifier extends cls { calcDepFlags(this.idealTree) } - // save the ideal's meta as a hidden lockfile after we actualize it - this.idealTree.meta.filename = - this.idealTree.realpath + '/node_modules/.package-lock.json' - this.idealTree.meta.hiddenLockfile = true - this.idealTree.meta.lockfileVersion = defaultLockfileVersion + // save the ideal's meta as a hidden lockfile after we actualize it. + // Under linked the logical tree is the hoisted layout, so the hidden lockfile is serialized from the isolated tree instead. + if (!linked) { + this.idealTree.meta.filename = + this.idealTree.realpath + '/node_modules/.package-lock.json' + this.idealTree.meta.hiddenLockfile = true + this.idealTree.meta.lockfileVersion = defaultLockfileVersion + } this.actualTree = this.idealTree this.idealTree = null if (!this.options.global && !this.options.dryRun) { - await this.actualTree.meta.save() + if (linked) { + await this.#saveLinkedHiddenLockfile(isolatedTree) + } else { + 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 // tree, then run the dependencies scripts @@ -452,7 +483,10 @@ module.exports = cls => class Reifier extends cls { } if (includeRootDeps) { // add all non-workspace nodes to filterNodes - for (const tree of [this.idealTree, this.actualTree]) { + // Skip the actual tree under the linked diff wrapper: its edge targets have root===actualTree, not the wrapper, which trips Diff.calculate's filterNode guard. + // The ideal-side targets alone scope the diff. + const trees = this.#linkedActualForDiff ? [this.idealTree] : [this.idealTree, this.actualTree] + for (const tree of trees) { for (const { type, to } of tree.edgesOut.values()) { if (type !== 'workspace' && to) { filterNodes.push(to) @@ -823,6 +857,47 @@ module.exports = cls => class Reifier extends cls { return join(filePath) } + // Serialize the hidden lockfile from the isolated tree, which mirrors the on-disk .store/symlink layout. + // Its children are every materialized node_modules entry: store package dirs and all symlinks. + async #saveLinkedHiddenLockfile (isolatedTree) { + const path = isolatedTree.realpath + const meta = new Shrinkwrap({ + path, + hiddenLockfile: true, + lockfileVersion: defaultLockfileVersion, + resolveOptions: this.options, + }) + meta.reset() + meta.filename = resolve(path, 'node_modules/.package-lock.json') + const storeRe = /^(.*\/\.store\/.+?)\/node_modules\// + const containers = new Set() + const nodes = new Set() + for (const node of isolatedTree.children.values()) { + // Tree-only undeclared workspace self-links aren't on disk. + if (node.isUndeclaredWorkspaceLink) { + continue + } + nodes.add(node) + // Record the enclosing .store/ dir so loadVirtual can resolve a store package's sibling deps. + // node.location uses the platform separator; lockfile keys are posix. + const m = node.location.replace(/\\/g, '/').match(storeRe) + if (m) { + containers.add(m[1]) + } + } + // Workspace dirs hold their own dep symlinks; record them so the cache can validate those subtrees. + for (const ws of isolatedTree.fsChildren) { + nodes.add(ws) + } + for (const node of nodes) { + meta.add(node) + } + for (const loc of containers) { + meta.data.packages[loc] = {} + } + await meta.save() + } + // Build a flat actual tree wrapper for linked installs so the diff can correctly match store entries that already exist on disk. // The proxy tree from createIsolatedTree() is flat (all children on root), but loadActual() produces a nested tree where store entries are deep link targets. // This wrapper surfaces them at the root level for comparison. @@ -841,6 +916,10 @@ module.exports = cls => class Reifier extends cls { if (child.isLink && child.resolved?.startsWith('file:.store/') && !existsSync(child.realpath)) { continue } + // Skip a link whose on-disk target is a valid-but-wrong store key (e.g. an interrupted update), so the diff repoints it via ADD. + if (child.isLink && this.#linkTargetMismatch(child)) { + continue + } let entry if (child.isLink) { entry = new IsolatedLink(child) @@ -880,6 +959,12 @@ module.exports = cls => class Reifier extends cls { return wrapper } + // True when the link's on-disk target resolves to a different path than its ideal target. + // The caller only invokes this once both paths exist, so realpathSync won't throw. + #linkTargetMismatch (child) { + return realpathSync(child.path) !== realpathSync(child.realpath) + } + // When extracting a registry-resolved package, the spec we hand to pacote is name@URL. // pacote re-parses that with npa and gets spec.type === 'remote', so without an override the allow-remote gate would fire on every registry tarball (both =none and =root mis-fire). // Returns true only when we are confident this is a registry-mediated install. @@ -888,12 +973,14 @@ module.exports = cls => class Reifier extends cls { return false } try { - const resolved = new URL(node.resolved) + // Match the effective fetch URL, not the raw lockfile value. + // #registryResolved applies replace-registry-host, rewriting a public-registry pin to the configured proxy/mirror so it matches. + const resolvedURL = new URL(this.#registryResolved(node.resolved)) // pickRegistry only consults spec.scope, so a bare-name (tag) parse is sufficient and avoids a node.version dependency. const registry = new URL(pickRegistry(npa(node.name), this.options)) const registryPath = registry.pathname.replace(/\/?$/, '/') - return resolved.origin === registry.origin && - (registryPath === '/' || resolved.pathname.startsWith(registryPath)) + return resolvedURL.origin === registry.origin && + (registryPath === '/' || resolvedURL.pathname.startsWith(registryPath)) } catch { return false } @@ -910,36 +997,42 @@ module.exports = cls => class Reifier extends cls { // the default reg as the magical animal that it has been. try { const resolvedURL = hgi.parseUrl(resolved) + const registryURL = new URL(this.registry) + const registryPath = registryURL.pathname.replace(/\/$/, '') - if ((this.options.replaceRegistryHost === resolvedURL.hostname) || - this.options.replaceRegistryHost === 'always') { - const registryURL = new URL(this.registry) + let matchURL = null + try { + matchURL = new URL(this.options.replaceRegistryHost) + } catch { + // keep matchURL null + } - // Replace the host with the registry host while keeping the path intact - resolvedURL.hostname = registryURL.hostname - resolvedURL.port = registryURL.port - resolvedURL.protocol = registryURL.protocol + const matchHost = matchURL?.hostname ?? this.options.replaceRegistryHost + const matchPath = matchURL?.pathname.replace(/\/$/, '') ?? null + const hasPathPrefix = (pathname, prefix) => + pathname === prefix || pathname.startsWith(`${prefix}/`) - // Make sure we don't double-include the path if it's already there - const registryPath = registryURL.pathname.replace(/\/$/, '') + const hostMatches = this.options.replaceRegistryHost === 'always' || matchHost === resolvedURL.hostname + const pathMatches = !matchPath || hasPathPrefix(resolvedURL.pathname, matchPath) - if (registryPath && registryPath !== '/') { - // Check if the resolved pathname already starts with the registry path - // We need to ensure it's a proper path prefix, not just a string prefix - // e.g., registry path '/npm' should not match '/npm-run-path' - const hasRegistryPath = resolvedURL.pathname === registryPath || - resolvedURL.pathname.startsWith(registryPath + '/') + if (!hostMatches || !pathMatches) { + return resolved + } - if (!hasRegistryPath) { - // Since hostname is changed, we need to ensure the registry path is included - resolvedURL.pathname = registryPath + resolvedURL.pathname - } - } + resolvedURL.protocol = registryURL.protocol + resolvedURL.hostname = registryURL.hostname + resolvedURL.port = registryURL.port - return resolvedURL.toString() + if (matchPath) { + // full-URL prefix: swap old path prefix for the registry path + resolvedURL.pathname = registryPath + resolvedURL.pathname.slice(matchPath.length) + } else if (registryPath && !hasPathPrefix(resolvedURL.pathname, registryPath)) { + // host-only: prepend registry path if not already present + resolvedURL.pathname = registryPath + resolvedURL.pathname } - return resolved - } catch (e) { + + return resolvedURL.toString() + } catch { // if we could not parse the url at all then returning nothing // here means it will get removed from the tree in the next step return undefined @@ -1150,7 +1243,8 @@ module.exports = cls => class Reifier extends cls { // with the reification, and be resolved at a later time. const timeEnd = time.start('reify:audit') const options = { ...this.options } - const tree = this.idealTree + // Under the linked strategy idealTree is the isolated tree, which the audit cannot traverse; audit the non-isolated tree instead. + const tree = this.#linkedIdealForAudit || this.idealTree // if we're operating on a workspace, only audit the workspace deps if (this.options.workspaces.length) { @@ -1357,6 +1451,18 @@ module.exports = cls => class Reifier extends cls { timeEnd() } + // Remove the root .store left behind by a previous linked install when reifying under a non-linked strategy. + async #removeStaleStoreDir () { + const storeDir = resolve(this.path, 'node_modules', '.store') + if (!existsSync(storeDir)) { + return + } + log.silly('reify', 'removing stale .store from a previous linked install') + await rm(storeDir, { recursive: true, force: true }) + .catch(/* istanbul ignore next -- rm with force rarely fails */ + er => log.warn('cleanup', 'Failed to remove stale .store directory', er)) + } + // After a linked install, scan node_modules/.store/ and remove any directories that are not referenced by the current ideal tree. // Store entries become orphaned when dependencies are updated or removed, because the diff never sees the old store keys. // Then sweep the top-level node_modules/ for orphaned symlinks (e.g. an uninstalled dep whose store entry was just removed) so we don't leave dangling links. @@ -1395,6 +1501,8 @@ module.exports = cls => class Reifier extends cls { // Locations are normalized to forward slashes here because IsolatedNode/IsolatedLink locations are built with path.join, which uses backslashes on Windows. const validKeys = new Set() const nmDirs = new Map() + // Valid bin shim names per node_modules dir, collected from each top-level entry's package.bin so the .bin sweep keeps only shims a still-installed package provides. + const binsByDir = new Map() const NM_PREFIX = 'node_modules/' const STORE_MARKER = '/.store/' for (const child of this.idealTree.children.values()) { @@ -1407,13 +1515,11 @@ module.exports = cls => class Reifier extends cls { validKeys.add(key) continue } - 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) { + if (child.isLink && child.isUndeclaredWorkspaceLink) { continue } + // Real top-level Nodes (e.g. the root's bundled deps) fall through here too, so they are recorded as valid and never swept as stale. const nmIdx = loc.lastIndexOf(NM_PREFIX) if (nmIdx === -1 || loc.includes(STORE_MARKER)) { continue @@ -1434,6 +1540,19 @@ module.exports = cls => class Reifier extends cls { nmDirs.set(dir, set) } set.add(entry) + + // package.bin is normalized to an object keyed by bin name; shim names are unscoped even for scoped packages. + const bin = child.package?.bin + if (bin && typeof bin === 'object') { + let binSet = binsByDir.get(dir) + if (!binSet) { + binSet = new Set() + binsByDir.set(dir, binSet) + } + for (const bn of Object.keys(bin)) { + binSet.add(bn) + } + } } // Determine which node_modules directories to sweep. @@ -1507,14 +1626,47 @@ module.exports = cls => class Reifier extends cls { for (const [dir, valid] of nmDirs) { await this.#cleanOrphanedTopLevelLinks(dir, valid) + await this.#cleanStaleBinLinks(dir, binsByDir.get(dir)) } } + // Remove stale bin shims left in node_modules/.bin after an uninstall under linked, where the diff never emits an action to drop them. + // A shim is stale when no still-linked package provides its name, or when it is a dangling symlink; matching by name handles both POSIX symlinks and Windows .cmd/.ps1 shims. + async #cleanStaleBinLinks (nmDir, validBins = new Set()) { + const binDir = resolve(nmDir, '.bin') + let names + try { + names = await readdir(binDir) + } catch { + return + } + + const stale = names.filter(name => { + const base = name.replace(/\.(cmd|ps1)$/, '') + return !validBins.has(base) || !existsSync(resolve(binDir, name)) + }) + + if (!stale.length) { + return + } + + log.silly('reify', 'cleaning stale bin links', stale) + await promiseAllRejectLate( + stale.map(name => + rm(resolve(binDir, name), { force: true }) + .catch(/* istanbul ignore next -- rm with force rarely fails */ + er => log.warn('cleanup', `Failed to remove stale bin link ${name}`, er)) + ) + ) + } + // Remove node_modules/ entries that aren't represented in the ideal tree. // Run for the project root and each workspace's node_modules. // The linked diff path can't see these because #buildLinkedActualForDiff derives the actual tree from the ideal, so removed deps are never compared. - // Only symlinks whose target resolves inside the project root are removed — that covers store links (node_modules/.store/...) and workspace self-links (e.g. node_modules/ -> ../packages/) that npm itself created. - // Symlinks pointing outside the project (e.g. `npm link foo` without --save targeting the global prefix, or hand-made `ln -s` to an external path) and real directories are preserved. + // Two kinds of stale entry are removed: + // - symlinks whose target resolves inside the project root — store links (node_modules/.store/...) and workspace self-links (e.g. node_modules/ -> ../packages/) that npm itself created. + // - real package directories — hoisted-layout deps left behind when switching from the hoisted strategy to linked, where every valid top-level entry is a symlink. + // Symlinks pointing outside the project (e.g. `npm link foo` without --save targeting the global prefix, or hand-made `ln -s` to an external path) and non-package real directories are preserved. async #cleanOrphanedTopLevelLinks (nmDir, validTopLevel) { const projectPrefix = resolve(this.path) + sep let dirents @@ -1535,7 +1687,15 @@ module.exports = cls => class Reifier extends cls { return resolve(dirname(linkPath), target).startsWith(projectPrefix) } + // A real directory is stale only when it is an actual package (has a package.json), so unrelated user directories are never touched. + const isStaleRealPkg = (dirent, entPath) => + dirent.isDirectory() && existsSync(resolve(entPath, 'package.json')) + + const isOrphan = async (dirent, entPath) => + (dirent.isSymbolicLink() && await isOurOrphan(entPath)) || isStaleRealPkg(dirent, entPath) + const orphaned = [] + const scopes = new Set() for (const ent of dirents) { // skip npm-managed entries (.bin, .store, .package-lock.json, etc) if (ent.name.startsWith('.')) { @@ -1551,11 +1711,12 @@ module.exports = cls => class Reifier extends cls { } for (const pkgEnt of scoped) { const key = `${ent.name}${sep}${pkgEnt.name}` - if (!validTopLevel.has(key) && pkgEnt.isSymbolicLink() && await isOurOrphan(resolve(nmDir, key))) { + if (!validTopLevel.has(key) && await isOrphan(pkgEnt, resolve(nmDir, key))) { orphaned.push(key) + scopes.add(ent.name) } } - } else if (!validTopLevel.has(ent.name) && ent.isSymbolicLink() && await isOurOrphan(resolve(nmDir, ent.name))) { + } else if (!validTopLevel.has(ent.name) && await isOrphan(ent, resolve(nmDir, ent.name))) { orphaned.push(ent.name) } } @@ -1564,14 +1725,29 @@ module.exports = cls => class Reifier extends cls { return } - log.silly('reify', 'cleaning orphaned top-level links', orphaned) + log.silly('reify', 'cleaning orphaned top-level entries', orphaned) await promiseAllRejectLate( orphaned.map(name => rm(resolve(nmDir, name), { recursive: true, force: true }) .catch(/* istanbul ignore next -- rm with force rarely fails */ - er => log.warn('cleanup', `Failed to remove orphaned link ${name}`, er)) + er => log.warn('cleanup', `Failed to remove orphaned entry ${name}`, er)) ) ) + + // Removing the last package under a scope leaves an empty @scope directory behind, so prune any scope directory that is now empty. + await promiseAllRejectLate( + [...scopes].map(async scope => { + const scopeDir = resolve(nmDir, scope) + try { + const remaining = await readdir(scopeDir) + if (!remaining.length) { + await rm(scopeDir, { recursive: true, force: true }) + } + } catch { + /* istanbul ignore next -- readdir of a scope dir we just listed should not fail */ + } + }) + ) } // last but not least, we save the ideal tree metadata to the package-lock diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/audit-report.js b/deps/npm/node_modules/@npmcli/arborist/lib/audit-report.js index 3e74e100b6c537..1c3e15dee1924c 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/audit-report.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/audit-report.js @@ -6,6 +6,7 @@ const pickManifest = require('npm-pick-manifest') const Vuln = require('./vuln.js') const Calculator = require('@npmcli/metavuln-calculator') +const { isReleaseAgeExcluded } = require('./release-age-exclude.js') const { log, time } = require('proc-log') @@ -151,11 +152,6 @@ class AuditReport extends Map { continue } - // we will have loaded the source already if this is a metavuln - if (advisory.type === 'metavuln') { - vuln.addVia(this.get(advisory.dependency)) - } - // already marked this one, no need to do it again if (vuln.nodes.has(node)) { continue @@ -173,6 +169,11 @@ class AuditReport extends Map { // depends on root. this.topVulns.set(vuln.name, vuln) vuln.topNodes.add(dep) + } else { + // An in-range fix exists, but a `min-release-age`/`before` + // window may put the patched version out of reach, leaving the + // vulnerable version installed. + vuln.fixBlockedByReleaseAge = this.#fixBlockedByReleaseAge(vuln, spec) } } else { // calculate a metavuln, if necessary @@ -213,6 +214,17 @@ class AuditReport extends Map { } } + // post-loop reconciliation: establish all via links + // done here to ensure all vulns exist, avoid redundant setter triggers, + // and correctly handle multiple paths and omission cleanups. + for (const vuln of this.values()) { + for (const advisory of vuln.advisories) { + if (advisory.type === 'metavuln') { + vuln.addVia(this.get(advisory.dependency)) + } + } + } + timeEnd() } @@ -251,6 +263,53 @@ class AuditReport extends Map { } } + // A fix that `#fixAvailable` reports as installable in-range can still be + // unreachable when a release-age window (`before` / `min-release-age`) is set, + // because the patched version was published after the cutoff. `npm audit fix` + // then resolves back to a version that is still vulnerable, so detect that + // here and let callers warn about it. Only meaningful when an in-range fix + // exists (fixAvailable === true). Returns the blocked fix as + // `{ version, before }`, or `false` when nothing is blocked. + #fixBlockedByReleaseAge (vuln, spec) { + const { before, minReleaseAgeExclude } = this.options + // No window, or this package is explicitly exempt from it. + if (!before || isReleaseAgeExcluded(vuln.name, minReleaseAgeExclude)) { + return false + } + + // For `npm:` aliases the fix resolves against the alias target, so feed + // pickManifest the underlying range rather than the alias spec (which it + // rejects). Mirrors `#fixAvailable`. + const specObj = npa(spec) + if (specObj.subSpec) { + spec = specObj.subSpec.rawSpec + } + + // The version that would be installed if the window were lifted. + const { version } = pickManifest(vuln.packument, spec, { + ...this.options, + before: null, + avoid: vuln.range, + }) + + // What resolution can actually reach within the window. This mirrors how + // `build-ideal-tree` re-resolves the edge (non-strict avoid + `before`). + let windowed + try { + windowed = pickManifest(vuln.packument, spec, { + ...this.options, + avoid: vuln.range, + }) + } catch { + // Nothing is old enough to install at all: the fix is blocked. + return { version, before } + } + + // `_shouldAvoid` means the best version available within the window is still + // in the vulnerable range, so the only non-vulnerable fix is too new. + return windowed._shouldAvoid ? { version, before } : false + } + set () { throw new Error('do not call AuditReport.set() directly') } diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/calc-dep-flags.js b/deps/npm/node_modules/@npmcli/arborist/lib/calc-dep-flags.js index 5f2484858094dc..965127f5ac9dd9 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/calc-dep-flags.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/calc-dep-flags.js @@ -35,12 +35,32 @@ const calcDepFlags = (tree, resetRoot = true) => { if (node.target == null) { continue } - node.target.dev = node.dev - node.target.optional = node.optional - node.target.devOptional = node.devOptional - node.target.peer = node.peer - node.target.extraneous = node.extraneous - queue.push(node.target) + // Only unset flags, matching the edge walk below, so multiple links to one target can't clobber a correct value. + let changed = false + if (node.target.dev && !node.dev) { + node.target.dev = false + changed = true + } + if (node.target.optional && !node.optional) { + node.target.optional = false + changed = true + } + if (node.target.devOptional && !node.devOptional) { + node.target.devOptional = false + changed = true + } + if (node.target.peer && !node.peer) { + node.target.peer = false + changed = true + } + if (node.target.extraneous && !node.extraneous) { + node.target.extraneous = false + changed = true + } + // queue target on first visit so its deps are walked + if (changed || !seen.has(node.target)) { + queue.push(node.target) + } continue } diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/isolated-classes.js b/deps/npm/node_modules/@npmcli/arborist/lib/isolated-classes.js index 3a2ade64bbb3e8..fc119119664d98 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/isolated-classes.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/isolated-classes.js @@ -2,13 +2,6 @@ const CaseInsensitiveMap = require('./case-insensitive-map.js') const { resolve } = require('node:path') -// fake lib/inventory.js -class IsolatedInventory extends Map { - query () { - return [] - } -} - // fake lib/node.js class IsolatedNode { binPaths = [] @@ -18,7 +11,7 @@ class IsolatedNode { fsChildren = new Set() hasShrinkwrap = false integrity = null - inventory = new IsolatedInventory() + inventory = new Map() isInStore = false inBundle = false isRegistryDependency = false diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/link.js b/deps/npm/node_modules/@npmcli/arborist/lib/link.js index 3824dc81ffb3cb..89f993bb169004 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/link.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/link.js @@ -109,21 +109,12 @@ class Link extends Node { // so this is a no-op [_loadDeps] () {} - // When a Link receives overrides (via edgesIn), forward them to the target node which holds the actual edgesOut — but only when the OverrideSet has at least one rule that names a dep the target actually depends on. - // Without this scope, the link forwards a generic ancestor OverrideSet that has no real effect on the target's edges, but still flips the target to "has overrides", which changes downstream `canReplaceWith` / placement decisions and causes `npm ci` to re-resolve lockfile-pinned edges from the registry. - // See npm/cli#9357. + // Forward overrides to the target only when a rule names a dep it needs, directly or transitively (npm/cli#9357, #9619). recalculateOutEdgesOverrides () { if (!this.target || !this.overrides) { return } - let hasMatchingRule = false - for (const rule of this.overrides.ruleset.values()) { - if (this.target.edgesOut.has(rule.name)) { - hasMatchingRule = true - break - } - } - if (!hasMatchingRule) { + if (!overrideMatchesSubtree(this.overrides, this.target)) { return } this.target.updateOverridesEdgeInAdded(this.overrides) @@ -143,4 +134,29 @@ class Link extends Node { } } +// True when an override rule applies to an edge reachable from the target at any depth. +// getEdgeRule matches on name and spec, returning the set itself when nothing applies, so a non-applicable version-qualified rule doesn't flip an intermediate node to "has overrides" (npm/cli#9357). +// Not a method: runs from the Node super-constructor, before subclass private methods exist. +const overrideMatchesSubtree = (overrides, target) => { + const seen = new Set() + const stack = [target] + while (stack.length) { + const node = stack.pop() + const resolved = node.isLink ? node.target : node + if (seen.has(resolved)) { + continue + } + seen.add(resolved) + for (const edge of resolved.edgesOut.values()) { + if (overrides.getEdgeRule(edge).name === edge.name) { + return true + } + if (edge.to) { + stack.push(edge.to) + } + } + } + return false +} + module.exports = Link diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/node.js b/deps/npm/node_modules/@npmcli/arborist/lib/node.js index 451ede1e52cbf6..87b0ac41740bd6 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/node.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/node.js @@ -927,7 +927,8 @@ class Node { isTop: srcTop, path: srcPath, } = sourceReference || {} - const thisDev = isTop && !globalTop && path + // A package in the linked strategy's .store is a transitive dependency that is structurally a tree top, but its devDependencies are never installed or required, so they must not be loaded. + const thisDev = isTop && !globalTop && path && !this.isInStore const srcDev = !sourceReference || srcTop && !srcGlobalTop && srcPath if (thisDev && srcDev) { this.#loadDepType(this.package.devDependencies, 'dev', ad) diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/query-selector-all.js b/deps/npm/node_modules/@npmcli/arborist/lib/query-selector-all.js index 3a6c15139d40bd..05faa551f76ee8 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/query-selector-all.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/query-selector-all.js @@ -798,7 +798,8 @@ const hasParent = (node, compareNodes) => { // Only match if the node has a link whose parent is the compareNode. Without this check, nodes deep in the store (linked strategy) would incorrectly match as children of root via their fsParent chain. if (node.isTop && (node.resolveParent === compareNode)) { for (const link of node.linksIn) { - if (link.parent === compareNode) { + // A store-backing node (linked strategy) is reached through its logical Link, which matches via edgesIn below, so the store node itself is never a direct child. + if (link.parent === compareNode && !node.isInStore) { return true } } diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js b/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js index 89e24339791f1e..8c9b3fe118a8ed 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js @@ -43,7 +43,9 @@ const isScriptAllowed = (node, policy) => { let anyDeny = false for (const [key, value] of Object.entries(policy)) { - if (!matches(node, key)) { + // Pass deny intent so matchRegistry can fail closed on an unverifiable + // version: a deny still blocks, an allow stays refused. + if (!matches(node, key, value === false)) { continue } if (value === false) { @@ -66,7 +68,7 @@ const isScriptAllowed = (node, policy) => { return null } -const matches = (node, key) => { +const matches = (node, key, failClosed) => { let parsed try { parsed = npa(key) @@ -78,7 +80,7 @@ const matches = (node, key) => { case 'tag': case 'range': case 'version': - return matchRegistry(node, parsed) + return matchRegistry(node, parsed, failClosed) case 'git': return matchGit(node, parsed) case 'file': @@ -132,7 +134,7 @@ const resolvedSourceSpecs = (node) => { return specs } -const matchRegistry = (node, parsed) => { +const matchRegistry = (node, parsed, failClosed) => { // 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. @@ -168,9 +170,14 @@ const matchRegistry = (node, parsed) => { if (parsed.fetchSpec === '*' || parsed.rawSpec === '' || parsed.rawSpec === '*') { return true } - if (!trusted.version || !isExactVersionDisjunction(parsed.fetchSpec)) { + if (!isExactVersionDisjunction(parsed.fetchSpec)) { return false } + // Unverifiable version (omit-lockfile-registry-resolved): a deny blocks, + // an allow is refused. + if (!trusted.version) { + return failClosed + } return semver.satisfies(trusted.version, parsed.fetchSpec, { loose: true }) } @@ -178,6 +185,10 @@ const matchRegistry = (node, parsed) => { /* istanbul ignore else: parsed.type at this point is always 'version'; the istanbul-ignored fallback below handles the impossible case. */ if (parsed.type === 'version') { + // Unverifiable version: a deny blocks, an allow is refused. + if (!trusted.version) { + return failClosed + } return trusted.version === parsed.fetchSpec } @@ -366,6 +377,7 @@ const trustedDisplay = (node) => { module.exports = isScriptAllowed module.exports.isScriptAllowed = isScriptAllowed +module.exports.matches = matches module.exports.isExactVersionDisjunction = isExactVersionDisjunction module.exports.getTrustedRegistryIdentity = getTrustedRegistryIdentity module.exports.resolvedSourceSpecs = resolvedSourceSpecs diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js b/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js index cf27bcf4ac726e..03cc86edbd4e92 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/shrinkwrap.js @@ -172,6 +172,35 @@ const assertNoNewer = async (path, data, lockTime, dir, seen) => { return } + // The walk above can't reach two linked-strategy layouts: a store package's sibling deps under .store//node_modules (.store is a skipped dot-dir), and an undeclared workspace not symlinked into root node_modules. Walk those dirs, derived from the lockfile entries. + // A dir reachable through a link entry is skipped: it was already reached (or, if the symlink is stale, correctly stays unseen so the lockfile is rejected). This keeps the hoisted strategy's stale-symlink detection intact, since there every workspace is a link target. + const linkTargets = new Set() + for (const loc in data.packages) { + const { link, resolved } = data.packages[loc] + if (link && resolved) { + linkTargets.add(resolved.replace(/\\/g, '/')) + } + } + const extraDirs = new Set() + for (const loc in data.packages) { + const store = loc.match(/^(.*\/\.store\/.+?)\/node_modules\//) + if (store) { + // .store/ is never walked but has no entry, so mark it seen. + seen.add(store[1]) + extraDirs.add(`${store[1]}/node_modules`) + continue + } + // A workspace/fsChild dir outside node_modules, e.g. packages/a. + const i = loc.indexOf('/node_modules/') + const root = i === -1 ? loc : loc.slice(0, i) + if (root && !/(^|\/)node_modules(\/|$)/.test(root) && !linkTargets.has(root)) { + extraDirs.add(root) + } + } + for (const rel of extraDirs) { + await assertNoNewer(path, data, lockTime, resolve(path, rel), seen) + } + // assert that all the entries in the lockfile were seen for (const loc in data.packages) { if (!seen.has(loc)) { diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/unreviewed-scripts.js b/deps/npm/node_modules/@npmcli/arborist/lib/unreviewed-scripts.js index 587d06bf8fe56b..f7bed387845ec0 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/unreviewed-scripts.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/unreviewed-scripts.js @@ -50,6 +50,13 @@ const collectUnreviewedScripts = async ({ // its own lifecycle scripts. continue } + if (node.inert) { + // Inert = an optional dep that can't be installed here (failed the + // os/cpu/libc or engine check, or failed to load). reify drops it + // before any script runs, so its install scripts never execute and it + // must not be flagged (npm/cli#9562). + continue + } const verdict = isScriptAllowed(node, resolvedPolicy) if (verdict === true || verdict === false) { diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/vuln.js b/deps/npm/node_modules/@npmcli/arborist/lib/vuln.js index 2bffe54f2dacdc..e6c6bbab383ea7 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/vuln.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/vuln.js @@ -41,6 +41,7 @@ class Vuln { this.effects = new Set() this.topNodes = new Set() this.nodes = new Set() + this.fixBlockedByReleaseAge = false this.addAdvisory(advisory) this.packument = advisory.packument this.versions = advisory.versions @@ -126,6 +127,9 @@ class Vuln { range: this.simpleRange, nodes: [...this.nodes].map(n => n.location).sort(localeCompare), fixAvailable: this.#fixAvailable, + ...(this.fixBlockedByReleaseAge + ? { fixBlockedByReleaseAge: this.fixBlockedByReleaseAge } + : {}), } } diff --git a/deps/npm/node_modules/@npmcli/arborist/package.json b/deps/npm/node_modules/@npmcli/arborist/package.json index 9a5e883cefe2f3..11c142a0092a4f 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.8.0", + "version": "9.9.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 a4933facff6315..2bb1713458af9f 100644 --- a/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js +++ b/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js @@ -341,6 +341,10 @@ const definitions = { user > global), so a higher-priority source can always relax or override a lower-priority one. + As with \`min-release-age\`, when this cutoff blocks a fix that + \`npm audit fix\` would install, npm keeps the vulnerable version, warns, + and exits with a non-zero code. + Packages whose names match \`min-release-age-exclude\` are exempt from this filter. `, @@ -1204,8 +1208,15 @@ const definitions = { necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. - linked: (experimental) install in node_modules/.store, link in place, - unhoisted. + linked: install in node_modules/.store, link in place, unhoisted. + + We recommend that package authors use \`--install-strategy=linked\` + during development to catch undeclared ("phantom") dependencies before + publishing: the isolated layout only exposes a package's declared + dependencies, so an \`import\` of a package that was never added to + \`package.json\` can fail instead of resolving by accident and shipping + broken. See [Catching undeclared ("phantom") + dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). `, flatten, }), @@ -1475,6 +1486,12 @@ const definitions = { \`github:\` dependency); when both apply, \`before\` wins within a single source and across sources the standard precedence rules apply. + When this window stops \`npm audit fix\` from installing a patched + version (because the fix was published too recently), npm keeps the + package at its vulnerable version, warns that the fix was blocked, and + exits with a non-zero code. To install the fix, add the package to + \`min-release-age-exclude\`, or relax \`min-release-age\` or \`before\`. + Packages whose names match \`min-release-age-exclude\` are exempt from this filter. `, @@ -1913,7 +1930,7 @@ const definitions = { }), 'replace-registry-host': new Definition('replace-registry-host', { default: 'npmjs', - hint: ' | hostname', + hint: ' | hostname | url', type: ['npmjs', 'never', 'always', String], description: ` Defines behavior for replacing the registry host in a lockfile with the @@ -1924,7 +1941,14 @@ const definitions = { "never", then use the registry value. If set to "always", then replace the registry host with the configured host every time. - You may also specify a bare hostname (e.g., "registry.npmjs.org"). + You may also specify a bare hostname (e.g., "registry.npmjs.org") to only + replace URLs coming from that host. + + You may also specify a full URL including a path (e.g., + "https://old-registry.example.com/npm/path"). In that case, resolved URLs + whose host and path begin with that prefix will have the entire prefix + replaced with the configured registry URL (host and path), without + duplicating path segments. `, flatten, }), @@ -2361,6 +2385,10 @@ const definitions = { always silently skipped; this setting only affects unreviewed entries. \`--ignore-scripts\` and \`--dangerously-allow-all-scripts\` both override this setting. + + Optional dependencies that cannot be installed on the current platform + or engine (a non-matching \`os\`, \`cpu\`, or \`libc\`) are not flagged, + because their install scripts never run. `, flatten, }), diff --git a/deps/npm/node_modules/@npmcli/config/package.json b/deps/npm/node_modules/@npmcli/config/package.json index 2d91ded3f9a149..360b0ab128a03c 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.11.0", + "version": "10.12.0", "files": [ "bin/", "lib/" diff --git a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js index 33063dd3552cd6..e4e5bd87666d1b 100644 --- a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js +++ b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js @@ -95,19 +95,23 @@ function gte(i, y) { function expand_(str, max, isTop) { /** @type {string[]} */ const expansions = []; - const m = (0, balanced_match_1.balanced)('{', '}', str); - if (!m) - return [str]; - // no need to expand pre, since it is guaranteed to be free of brace-sets - const pre = m.pre; - const post = m.post.length ? expand_(m.post, max, false) : ['']; - if (/\$$/.test(m.pre)) { - for (let k = 0; k < post.length && k < max; k++) { - const expansion = pre + '{' + m.body + '}' + post[k]; - expansions.push(expansion); + // The `{a},b}` rewrite below restarts expansion on a rewritten string with + // the same `max` and `isTop = true`. Loop instead of recursing so a long run + // of non-expanding `{}` groups can't exhaust the call stack. + for (;;) { + const m = (0, balanced_match_1.balanced)('{', '}', str); + if (!m) + return [str]; + // no need to expand pre, since it is guaranteed to be free of brace-sets + const pre = m.pre; + if (/\$$/.test(m.pre)) { + const post = m.post.length ? expand_(m.post, max, false) : ['']; + for (let k = 0; k < post.length && k < max; k++) { + const expansion = pre + '{' + m.body + '}' + post[k]; + expansions.push(expansion); + } + return expansions; } - } - else { const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); const isSequence = isNumericSequence || isAlphaSequence; @@ -116,10 +120,16 @@ function expand_(str, max, isTop) { // {a},b} if (m.post.match(/,(?!,).*\}/)) { str = m.pre + '{' + m.body + escClose + m.post; - return expand_(str, max, true); + isTop = true; + continue; } return [str]; } + // Only expand post once we know this brace set actually expands. Computing + // it before the early returns above expanded post a second time on every + // non-expanding `{}`, which is what made inputs like `a{},{},{}...` blow up + // exponentially. + const post = m.post.length ? expand_(m.post, max, false) : ['']; let n; if (isSequence) { n = m.body.split(/\.\./); @@ -195,7 +205,7 @@ function expand_(str, max, isTop) { } } } + return expansions; } - return expansions; } //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/brace-expansion/dist/esm/index.js b/deps/npm/node_modules/brace-expansion/dist/esm/index.js index 32399e7b2f5cf1..b2d2aa91bb1c31 100644 --- a/deps/npm/node_modules/brace-expansion/dist/esm/index.js +++ b/deps/npm/node_modules/brace-expansion/dist/esm/index.js @@ -91,19 +91,23 @@ function gte(i, y) { function expand_(str, max, isTop) { /** @type {string[]} */ const expansions = []; - const m = balanced('{', '}', str); - if (!m) - return [str]; - // no need to expand pre, since it is guaranteed to be free of brace-sets - const pre = m.pre; - const post = m.post.length ? expand_(m.post, max, false) : ['']; - if (/\$$/.test(m.pre)) { - for (let k = 0; k < post.length && k < max; k++) { - const expansion = pre + '{' + m.body + '}' + post[k]; - expansions.push(expansion); + // The `{a},b}` rewrite below restarts expansion on a rewritten string with + // the same `max` and `isTop = true`. Loop instead of recursing so a long run + // of non-expanding `{}` groups can't exhaust the call stack. + for (;;) { + const m = balanced('{', '}', str); + if (!m) + return [str]; + // no need to expand pre, since it is guaranteed to be free of brace-sets + const pre = m.pre; + if (/\$$/.test(m.pre)) { + const post = m.post.length ? expand_(m.post, max, false) : ['']; + for (let k = 0; k < post.length && k < max; k++) { + const expansion = pre + '{' + m.body + '}' + post[k]; + expansions.push(expansion); + } + return expansions; } - } - else { const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); const isSequence = isNumericSequence || isAlphaSequence; @@ -112,10 +116,16 @@ function expand_(str, max, isTop) { // {a},b} if (m.post.match(/,(?!,).*\}/)) { str = m.pre + '{' + m.body + escClose + m.post; - return expand_(str, max, true); + isTop = true; + continue; } return [str]; } + // Only expand post once we know this brace set actually expands. Computing + // it before the early returns above expanded post a second time on every + // non-expanding `{}`, which is what made inputs like `a{},{},{}...` blow up + // exponentially. + const post = m.post.length ? expand_(m.post, max, false) : ['']; let n; if (isSequence) { n = m.body.split(/\.\./); @@ -191,7 +201,7 @@ function expand_(str, max, isTop) { } } } + return expansions; } - return expansions; } //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/brace-expansion/package.json b/deps/npm/node_modules/brace-expansion/package.json index 81524809e58612..a5142f2787b665 100644 --- a/deps/npm/node_modules/brace-expansion/package.json +++ b/deps/npm/node_modules/brace-expansion/package.json @@ -1,7 +1,7 @@ { "name": "brace-expansion", "description": "Brace expansion as known from sh/bash", - "version": "5.0.6", + "version": "5.0.7", "files": [ "dist" ], @@ -59,6 +59,6 @@ "module": "./dist/esm/index.js", "repository": { "type": "git", - "url": "git+ssh://git@github.com/juliangruber/brace-expansion.git" + "url": "git+https://github.com/juliangruber/brace-expansion.git" } } diff --git a/deps/npm/node_modules/libnpmdiff/package.json b/deps/npm/node_modules/libnpmdiff/package.json index 0e2a7968c74c7a..31aadc1a771808 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.10", + "version": "8.1.11", "description": "The registry diff", "repository": { "type": "git", @@ -47,7 +47,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.8.0", + "@npmcli/arborist": "^9.9.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 35452f796246b8..4d7c1266546891 100644 --- a/deps/npm/node_modules/libnpmexec/lib/index.js +++ b/deps/npm/node_modules/libnpmexec/lib/index.js @@ -19,8 +19,6 @@ const runScript = require('./run-script.js') const isWindows = require('./is-windows.js') const withLock = require('./with-lock.js') -const binPaths = [] - // when checking the local tree we look up manifests, cache those results by // spec.raw so we don't have to fetch again when we check npxCache const manifests = new Map() @@ -113,6 +111,8 @@ const exec = async (opts) => { ...flatOptions } = opts + const binPaths = [] + let pkgPaths = opts.pkgPath if (typeof pkgPaths === 'string') { pkgPaths = [pkgPaths] @@ -196,7 +196,11 @@ const exec = async (opts) => { let commandManifest await Promise.all(packages.map(async (pkg, i) => { const spec = npa(pkg, path) - const { manifest, node } = await missingFromTree({ spec, tree: localTree, flatOptions }) + const { manifest, node } = await missingFromTree({ + spec, + tree: localTree, + flatOptions, + }) if (manifest) { // Package does not exist in the local tree needInstall.push({ spec, manifest }) diff --git a/deps/npm/node_modules/libnpmexec/package.json b/deps/npm/node_modules/libnpmexec/package.json index 77af2f57ccd508..097673618ae8dd 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.3.0", + "version": "10.3.1", "files": [ "bin/", "lib/" @@ -61,7 +61,7 @@ }, "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.8.0", + "@npmcli/arborist": "^9.9.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 a03c4e89d63fb2..a6dc15e4b727c9 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.24", + "version": "7.0.25", "main": "lib/index.js", "files": [ "bin/", @@ -46,7 +46,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.8.0" + "@npmcli/arborist": "^9.9.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 5c285836d35364..92aa96af6408ea 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.10", + "version": "9.1.11", "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.8.0", + "@npmcli/arborist": "^9.9.0", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" diff --git a/deps/npm/node_modules/npm-profile/lib/index.js b/deps/npm/node_modules/npm-profile/lib/index.js index 83ab5e1b46b68e..734e08abb655ff 100644 --- a/deps/npm/node_modules/npm-profile/lib/index.js +++ b/deps/npm/node_modules/npm-profile/lib/index.js @@ -49,6 +49,33 @@ const isValidUrl = u => { } } +// npm's web-login response names the canonical npmjs registry in `doneUrl`, which a proxy/mirror forwards verbatim. +// The poll would then hit npmjs.org instead of the proxy that holds the session, so rewrite only that npmjs host to the configured registry origin, preserving the path prefix and query string. +// Any other done host is left untouched, since a non-npmjs canonical host cannot be inferred here and may be served intentionally. +const CANONICAL_REGISTRY_HOST = 'registry.npmjs.org' + +// doneUrl is already validated by isValidUrl and registry is the origin a prior +// POST /-/v1/login succeeded against, so both parse cleanly here. +const replaceDoneUrlOrigin = (doneUrl, registry) => { + if (!registry) { + return doneUrl + } + const done = new URL(doneUrl) + if (done.hostname !== CANONICAL_REGISTRY_HOST) { + return doneUrl + } + const reg = new URL(registry) + done.protocol = reg.protocol + done.host = reg.host + const prefix = reg.pathname.replace(/\/$/, '') + if (prefix && prefix !== '/' && + done.pathname !== prefix && + !done.pathname.startsWith(prefix + '/')) { + done.pathname = prefix + done.pathname + } + return done.href +} + const webAuth = async (opener, opts, body) => { try { const res = await fetch('/-/v1/login', { @@ -65,7 +92,7 @@ const webAuth = async (opener, opts, body) => { throw new WebLoginInvalidResponse('POST', res, content) } - return await webAuthOpener(opener, loginUrl, doneUrl, opts) + return await webAuthOpener(opener, loginUrl, replaceDoneUrlOrigin(doneUrl, opts.registry), opts) } catch (er) { if ((er.statusCode >= 400 && er.statusCode <= 499) || er.statusCode === 500) { throw new WebLoginNotSupported('POST', { diff --git a/deps/npm/node_modules/npm-profile/package.json b/deps/npm/node_modules/npm-profile/package.json index 0f97cc1efa1934..bb7f23ec83121b 100644 --- a/deps/npm/node_modules/npm-profile/package.json +++ b/deps/npm/node_modules/npm-profile/package.json @@ -1,13 +1,13 @@ { "name": "npm-profile", - "version": "12.0.1", + "version": "12.0.2", "description": "Library for updating an npmjs.com profile", "keywords": [], "author": "GitHub Inc.", "license": "ISC", "dependencies": { "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0" + "proc-log": "^6.1.0" }, "main": "./lib/index.js", "repository": { @@ -19,8 +19,8 @@ "lib/" ], "devDependencies": { - "@npmcli/eslint-config": "^5.0.0", - "@npmcli/template-oss": "4.27.1", + "@npmcli/eslint-config": "^7.0.0", + "@npmcli/template-oss": "5.1.1", "nock": "^13.5.6", "tap": "^16.0.1" }, @@ -46,7 +46,7 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.27.1", + "version": "5.1.1", "publish": true } } diff --git a/deps/npm/node_modules/semver/classes/range.js b/deps/npm/node_modules/semver/classes/range.js index 766d505a226919..a7d6556febb4e5 100644 --- a/deps/npm/node_modules/semver/classes/range.js +++ b/deps/npm/node_modules/semver/classes/range.js @@ -299,6 +299,10 @@ const replaceTildes = (comp, options) => { const replaceTilde = (comp, options) => { const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] + // if we're including prereleases in the match, then the lower bound is + // -0, the lowest possible prerelease value, just like x-ranges and carets. + // this keeps `~1.2` equivalent to the `1.2.x` x-range it's documented as. + const z = options.includePrerelease ? '-0' : '' return comp.replace(r, (_, M, m, p, pr) => { debug('tilde', comp, _, M, m, p, pr) let ret @@ -306,10 +310,10 @@ const replaceTilde = (comp, options) => { if (isX(M)) { ret = '' } else if (isX(m)) { - ret = `>=${M}.0.0 <${+M + 1}.0.0-0` + ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0` } else if (isX(p)) { // ~1.2 == >=1.2.0 <1.3.0-0 - ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0` + ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0` } else if (pr) { debug('replaceTilde pr', pr) ret = `>=${M}.${m}.${p}-${pr diff --git a/deps/npm/node_modules/semver/package.json b/deps/npm/node_modules/semver/package.json index ddedbf7bdaba6f..0cb7c7bb465ea4 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.4", + "version": "7.8.5", "description": "The semantic version parser used by npm.", "main": "index.js", "scripts": { diff --git a/deps/npm/node_modules/tar/dist/commonjs/header.js b/deps/npm/node_modules/tar/dist/commonjs/header.js index d6e8f0ec342678..f4a84fb76c3462 100644 --- a/deps/npm/node_modules/tar/dist/commonjs/header.js +++ b/deps/npm/node_modules/tar/dist/commonjs/header.js @@ -41,6 +41,7 @@ exports.Header = void 0; const node_path_1 = require("node:path"); const large = __importStar(require("./large-numbers.js")); const types = __importStar(require("./types.js")); +const notNegative = (n) => n === undefined || n < 0 ? undefined : n; class Header { cksumValid = false; needPax = false; @@ -99,10 +100,9 @@ class Header { exForFields?.uid ?? gexForFields?.uid ?? decNumber(buf, off + 108, 8); this.gid = exForFields?.gid ?? gexForFields?.gid ?? decNumber(buf, off + 116, 8); - this.size = - exForFields?.size ?? - gexForFields?.size ?? - decNumber(buf, off + 124, 12); + this.size = notNegative(exForFields?.size ?? + gexForFields?.size ?? + decNumber(buf, off + 124, 12)); this.mtime = exForFields?.mtime ?? gexForFields?.mtime ?? @@ -188,6 +188,7 @@ class Header { // null/undefined values are ignored. return !(v === null || v === undefined || + (k === 'size' && Number(v) < 0) || (k === 'path' && gex) || (k === 'linkpath' && gex) || k === 'global'); diff --git a/deps/npm/node_modules/tar/dist/commonjs/index.min.js b/deps/npm/node_modules/tar/dist/commonjs/index.min.js index 6bce016bcece46..1a229a01e5a5f8 100644 --- a/deps/npm/node_modules/tar/dist/commonjs/index.min.js +++ b/deps/npm/node_modules/tar/dist/commonjs/index.min.js @@ -1,4 +1,4 @@ -"use strict";var d=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var We=d(F=>{"use strict";var Ro=F&&F.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(F,"__esModule",{value:!0});F.Minipass=F.isWritable=F.isReadable=F.isStream=void 0;var Br=typeof process=="object"&&process?process:{stdout:null,stderr:null},is=require("node:events"),jr=Ro(require("node:stream")),vo=require("node:string_decoder"),To=s=>!!s&&typeof s=="object"&&(s instanceof Zt||s instanceof jr.default||(0,F.isReadable)(s)||(0,F.isWritable)(s));F.isStream=To;var Do=s=>!!s&&typeof s=="object"&&s instanceof is.EventEmitter&&typeof s.pipe=="function"&&s.pipe!==jr.default.Writable.prototype.pipe;F.isReadable=Do;var Po=s=>!!s&&typeof s=="object"&&s instanceof is.EventEmitter&&typeof s.write=="function"&&typeof s.end=="function";F.isWritable=Po;var le=Symbol("EOF"),ue=Symbol("maybeEmitEnd"),_e=Symbol("emittedEnd"),xt=Symbol("emittingEnd"),dt=Symbol("emittedError"),jt=Symbol("closed"),zr=Symbol("read"),Ut=Symbol("flush"),kr=Symbol("flushChunk"),K=Symbol("encoding"),Ue=Symbol("decoder"),O=Symbol("flowing"),mt=Symbol("paused"),qe=Symbol("resume"),R=Symbol("buffer"),I=Symbol("pipes"),v=Symbol("bufferLength"),$i=Symbol("bufferPush"),qt=Symbol("bufferShift"),N=Symbol("objectMode"),y=Symbol("destroyed"),Xi=Symbol("error"),Qi=Symbol("emitData"),xr=Symbol("emitEnd"),Ji=Symbol("emitEnd2"),J=Symbol("async"),es=Symbol("abort"),Wt=Symbol("aborted"),pt=Symbol("signal"),Ne=Symbol("dataListeners"),x=Symbol("discarded"),_t=s=>Promise.resolve().then(s),No=s=>s(),Mo=s=>s==="end"||s==="finish"||s==="prefinish",Lo=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,Ao=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),Ht=class{src;dest;opts;ondrain;constructor(e,t,i){this.src=e,this.dest=t,this.opts=i,this.ondrain=()=>e[qe](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(e){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},ts=class extends Ht{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(e,t,i){super(e,t,i),this.proxyErrors=r=>this.dest.emit("error",r),e.on("error",this.proxyErrors)}},Io=s=>!!s.objectMode,Fo=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",Zt=class extends is.EventEmitter{[O]=!1;[mt]=!1;[I]=[];[R]=[];[N];[K];[J];[Ue];[le]=!1;[_e]=!1;[xt]=!1;[jt]=!1;[dt]=null;[v]=0;[y]=!1;[pt];[Wt]=!1;[Ne]=0;[x]=!1;writable=!0;readable=!0;constructor(...e){let t=e[0]||{};if(super(),t.objectMode&&typeof t.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Io(t)?(this[N]=!0,this[K]=null):Fo(t)?(this[K]=t.encoding,this[N]=!1):(this[N]=!1,this[K]=null),this[J]=!!t.async,this[Ue]=this[K]?new vo.StringDecoder(this[K]):null,t&&t.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[R]}),t&&t.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[I]});let{signal:i}=t;i&&(this[pt]=i,i.aborted?this[es]():i.addEventListener("abort",()=>this[es]()))}get bufferLength(){return this[v]}get encoding(){return this[K]}set encoding(e){throw new Error("Encoding must be set at instantiation time")}setEncoding(e){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[N]}set objectMode(e){throw new Error("objectMode must be set at instantiation time")}get async(){return this[J]}set async(e){this[J]=this[J]||!!e}[es](){this[Wt]=!0,this.emit("abort",this[pt]?.reason),this.destroy(this[pt]?.reason)}get aborted(){return this[Wt]}set aborted(e){}write(e,t,i){if(this[Wt])return!1;if(this[le])throw new Error("write after end");if(this[y])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof t=="function"&&(i=t,t="utf8"),t||(t="utf8");let r=this[J]?_t:No;if(!this[N]&&!Buffer.isBuffer(e)){if(Ao(e))e=Buffer.from(e.buffer,e.byteOffset,e.byteLength);else if(Lo(e))e=Buffer.from(e);else if(typeof e!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[N]?(this[O]&&this[v]!==0&&this[Ut](!0),this[O]?this.emit("data",e):this[$i](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[O]):e.length?(typeof e=="string"&&!(t===this[K]&&!this[Ue]?.lastNeed)&&(e=Buffer.from(e,t)),Buffer.isBuffer(e)&&this[K]&&(e=this[Ue].write(e)),this[O]&&this[v]!==0&&this[Ut](!0),this[O]?this.emit("data",e):this[$i](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[O]):(this[v]!==0&&this.emit("readable"),i&&r(i),this[O])}read(e){if(this[y])return null;if(this[x]=!1,this[v]===0||e===0||e&&e>this[v])return this[ue](),null;this[N]&&(e=null),this[R].length>1&&!this[N]&&(this[R]=[this[K]?this[R].join(""):Buffer.concat(this[R],this[v])]);let t=this[zr](e||null,this[R][0]);return this[ue](),t}[zr](e,t){if(this[N])this[qt]();else{let i=t;e===i.length||e===null?this[qt]():typeof i=="string"?(this[R][0]=i.slice(e),t=i.slice(0,e),this[v]-=e):(this[R][0]=i.subarray(e),t=i.subarray(0,e),this[v]-=e)}return this.emit("data",t),!this[R].length&&!this[le]&&this.emit("drain"),t}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t="utf8"),e!==void 0&&this.write(e,t),i&&this.once("end",i),this[le]=!0,this.writable=!1,(this[O]||!this[mt])&&this[ue](),this}[qe](){this[y]||(!this[Ne]&&!this[I].length&&(this[x]=!0),this[mt]=!1,this[O]=!0,this.emit("resume"),this[R].length?this[Ut]():this[le]?this[ue]():this.emit("drain"))}resume(){return this[qe]()}pause(){this[O]=!1,this[mt]=!0,this[x]=!1}get destroyed(){return this[y]}get flowing(){return this[O]}get paused(){return this[mt]}[$i](e){this[N]?this[v]+=1:this[v]+=e.length,this[R].push(e)}[qt](){return this[N]?this[v]-=1:this[v]-=this[R][0].length,this[R].shift()}[Ut](e=!1){do;while(this[kr](this[qt]())&&this[R].length);!e&&!this[R].length&&!this[le]&&this.emit("drain")}[kr](e){return this.emit("data",e),this[O]}pipe(e,t){if(this[y])return e;this[x]=!1;let i=this[_e];return t=t||{},e===Br.stdout||e===Br.stderr?t.end=!1:t.end=t.end!==!1,t.proxyErrors=!!t.proxyErrors,i?t.end&&e.end():(this[I].push(t.proxyErrors?new ts(this,e,t):new Ht(this,e,t)),this[J]?_t(()=>this[qe]()):this[qe]()),e}unpipe(e){let t=this[I].find(i=>i.dest===e);t&&(this[I].length===1?(this[O]&&this[Ne]===0&&(this[O]=!1),this[I]=[]):this[I].splice(this[I].indexOf(t),1),t.unpipe())}addListener(e,t){return this.on(e,t)}on(e,t){let i=super.on(e,t);if(e==="data")this[x]=!1,this[Ne]++,!this[I].length&&!this[O]&&this[qe]();else if(e==="readable"&&this[v]!==0)super.emit("readable");else if(Mo(e)&&this[_e])super.emit(e),this.removeAllListeners(e);else if(e==="error"&&this[dt]){let r=t;this[J]?_t(()=>r.call(this,this[dt])):r.call(this,this[dt])}return i}removeListener(e,t){return this.off(e,t)}off(e,t){let i=super.off(e,t);return e==="data"&&(this[Ne]=this.listeners("data").length,this[Ne]===0&&!this[x]&&!this[I].length&&(this[O]=!1)),i}removeAllListeners(e){let t=super.removeAllListeners(e);return(e==="data"||e===void 0)&&(this[Ne]=0,!this[x]&&!this[I].length&&(this[O]=!1)),t}get emittedEnd(){return this[_e]}[ue](){!this[xt]&&!this[_e]&&!this[y]&&this[R].length===0&&this[le]&&(this[xt]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[jt]&&this.emit("close"),this[xt]=!1)}emit(e,...t){let i=t[0];if(e!=="error"&&e!=="close"&&e!==y&&this[y])return!1;if(e==="data")return!this[N]&&!i?!1:this[J]?(_t(()=>this[Qi](i)),!0):this[Qi](i);if(e==="end")return this[xr]();if(e==="close"){if(this[jt]=!0,!this[_e]&&!this[y])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(e==="error"){this[dt]=i,super.emit(Xi,i);let n=!this[pt]||this.listeners("error").length?super.emit("error",i):!1;return this[ue](),n}else if(e==="resume"){let n=super.emit("resume");return this[ue](),n}else if(e==="finish"||e==="prefinish"){let n=super.emit(e);return this.removeAllListeners(e),n}let r=super.emit(e,...t);return this[ue](),r}[Qi](e){for(let i of this[I])i.dest.write(e)===!1&&this.pause();let t=this[x]?!1:super.emit("data",e);return this[ue](),t}[xr](){return this[_e]?!1:(this[_e]=!0,this.readable=!1,this[J]?(_t(()=>this[Ji]()),!0):this[Ji]())}[Ji](){if(this[Ue]){let t=this[Ue].end();if(t){for(let i of this[I])i.dest.write(t);this[x]||super.emit("data",t)}}for(let t of this[I])t.end();let e=super.emit("end");return this.removeAllListeners("end"),e}async collect(){let e=Object.assign([],{dataLength:0});this[N]||(e.dataLength=0);let t=this.promise();return this.on("data",i=>{e.push(i),this[N]||(e.dataLength+=i.length)}),await t,e}async concat(){if(this[N])throw new Error("cannot concat in objectMode");let e=await this.collect();return this[K]?e.join(""):Buffer.concat(e,e.dataLength)}async promise(){return new Promise((e,t)=>{this.on(y,()=>t(new Error("stream destroyed"))),this.on("error",i=>t(i)),this.on("end",()=>e())})}[Symbol.asyncIterator](){this[x]=!1;let e=!1,t=async()=>(this.pause(),e=!0,{value:void 0,done:!0});return{next:()=>{if(e)return t();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[le])return t();let n,o,h=c=>{this.off("data",a),this.off("end",l),this.off(y,u),t(),o(c)},a=c=>{this.off("error",h),this.off("end",l),this.off(y,u),this.pause(),n({value:c,done:!!this[le]})},l=()=>{this.off("error",h),this.off("data",a),this.off(y,u),t(),n({done:!0,value:void 0})},u=()=>h(new Error("stream destroyed"));return new Promise((c,E)=>{o=E,n=c,this.once(y,u),this.once("error",h),this.once("end",l),this.once("data",a)})},throw:t,return:t,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[x]=!1;let e=!1,t=()=>(this.pause(),this.off(Xi,t),this.off(y,t),this.off("end",t),e=!0,{done:!0,value:void 0}),i=()=>{if(e)return t();let r=this.read();return r===null?t():{done:!1,value:r}};return this.once("end",t),this.once(Xi,t),this.once(y,t),{next:i,throw:t,return:t,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(e){if(this[y])return e?this.emit("error",e):this.emit(y),this;this[y]=!0,this[x]=!0,this[R].length=0,this[v]=0;let t=this;return typeof t.close=="function"&&!this[jt]&&t.close(),e?this.emit("error",e):this.emit(y),this}static get isStream(){return F.isStream}};F.Minipass=Zt});var Ke=d(W=>{"use strict";var Ur=W&&W.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(W,"__esModule",{value:!0});W.WriteStreamSync=W.WriteStream=W.ReadStreamSync=W.ReadStream=void 0;var Co=Ur(require("events")),z=Ur(require("fs")),Bo=We(),zo=z.default.writev,ye=Symbol("_autoClose"),$=Symbol("_close"),wt=Symbol("_ended"),p=Symbol("_fd"),ss=Symbol("_finished"),fe=Symbol("_flags"),rs=Symbol("_flush"),hs=Symbol("_handleChunk"),ls=Symbol("_makeBuf"),Et=Symbol("_mode"),Gt=Symbol("_needDrain"),Ge=Symbol("_onerror"),Ye=Symbol("_onopen"),ns=Symbol("_onread"),He=Symbol("_onwrite"),Ee=Symbol("_open"),V=Symbol("_path"),we=Symbol("_pos"),ee=Symbol("_queue"),Ze=Symbol("_read"),os=Symbol("_readSize"),ce=Symbol("_reading"),yt=Symbol("_remain"),as=Symbol("_size"),Yt=Symbol("_write"),Me=Symbol("_writing"),Kt=Symbol("_defaultFlag"),Le=Symbol("_errored"),Vt=class extends Bo.Minipass{[Le]=!1;[p];[V];[os];[ce]=!1;[as];[yt];[ye];constructor(e,t){if(t=t||{},super(t),this.readable=!0,this.writable=!1,typeof e!="string")throw new TypeError("path must be a string");this[Le]=!1,this[p]=typeof t.fd=="number"?t.fd:void 0,this[V]=e,this[os]=t.readSize||16*1024*1024,this[ce]=!1,this[as]=typeof t.size=="number"?t.size:1/0,this[yt]=this[as],this[ye]=typeof t.autoClose=="boolean"?t.autoClose:!0,typeof this[p]=="number"?this[Ze]():this[Ee]()}get fd(){return this[p]}get path(){return this[V]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[Ee](){z.default.open(this[V],"r",(e,t)=>this[Ye](e,t))}[Ye](e,t){e?this[Ge](e):(this[p]=t,this.emit("open",t),this[Ze]())}[ls](){return Buffer.allocUnsafe(Math.min(this[os],this[yt]))}[Ze](){if(!this[ce]){this[ce]=!0;let e=this[ls]();if(e.length===0)return process.nextTick(()=>this[ns](null,0,e));z.default.read(this[p],e,0,e.length,null,(t,i,r)=>this[ns](t,i,r))}}[ns](e,t,i){this[ce]=!1,e?this[Ge](e):this[hs](t,i)&&this[Ze]()}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,z.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}[Ge](e){this[ce]=!0,this[$](),this.emit("error",e)}[hs](e,t){let i=!1;return this[yt]-=e,e>0&&(i=super.write(ethis[Ye](e,t))}[Ye](e,t){this[Kt]&&this[fe]==="r+"&&e&&e.code==="ENOENT"?(this[fe]="w",this[Ee]()):e?this[Ge](e):(this[p]=t,this.emit("open",t),this[Me]||this[rs]())}end(e,t){return e&&this.write(e,t),this[wt]=!0,!this[Me]&&!this[ee].length&&typeof this[p]=="number"&&this[He](null,0),this}write(e,t){return typeof e=="string"&&(e=Buffer.from(e,t)),this[wt]?(this.emit("error",new Error("write() after end()")),!1):this[p]===void 0||this[Me]||this[ee].length?(this[ee].push(e),this[Gt]=!0,!1):(this[Me]=!0,this[Yt](e),!0)}[Yt](e){z.default.write(this[p],e,0,e.length,this[we],(t,i)=>this[He](t,i))}[He](e,t){e?this[Ge](e):(this[we]!==void 0&&typeof t=="number"&&(this[we]+=t),this[ee].length?this[rs]():(this[Me]=!1,this[wt]&&!this[ss]?(this[ss]=!0,this[$](),this.emit("finish")):this[Gt]&&(this[Gt]=!1,this.emit("drain"))))}[rs](){if(this[ee].length===0)this[wt]&&this[He](null,0);else if(this[ee].length===1)this[Yt](this[ee].pop());else{let e=this[ee];this[ee]=[],zo(this[p],e,this[we],(t,i)=>this[He](t,i))}}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,z.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}};W.WriteStream=$t;var cs=class extends $t{[Ee](){let e;if(this[Kt]&&this[fe]==="r+")try{e=z.default.openSync(this[V],this[fe],this[Et])}catch(t){if(t?.code==="ENOENT")return this[fe]="w",this[Ee]();throw t}else e=z.default.openSync(this[V],this[fe],this[Et]);this[Ye](null,e)}[$](){if(this[ye]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,z.default.closeSync(e),this.emit("close")}}[Yt](e){let t=!0;try{this[He](null,z.default.writeSync(this[p],e,0,e.length,this[we])),t=!1}finally{if(t)try{this[$]()}catch{}}}};W.WriteStreamSync=cs});var Xt=d(b=>{"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.dealias=b.isNoFile=b.isFile=b.isAsync=b.isSync=b.isAsyncNoFile=b.isSyncNoFile=b.isAsyncFile=b.isSyncFile=void 0;var ko=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),xo=s=>!!s.sync&&!!s.file;b.isSyncFile=xo;var jo=s=>!s.sync&&!!s.file;b.isAsyncFile=jo;var Uo=s=>!!s.sync&&!s.file;b.isSyncNoFile=Uo;var qo=s=>!s.sync&&!s.file;b.isAsyncNoFile=qo;var Wo=s=>!!s.sync;b.isSync=Wo;var Ho=s=>!s.sync;b.isAsync=Ho;var Zo=s=>!!s.file;b.isFile=Zo;var Go=s=>!s.file;b.isNoFile=Go;var Yo=s=>{let e=ko.get(s);return e||s},Ko=(s={})=>{if(!s)return{};let e={};for(let[t,i]of Object.entries(s)){let r=Yo(t);e[r]=i}return e.chmod===void 0&&e.noChmod===!1&&(e.chmod=!0),delete e.noChmod,e};b.dealias=Ko});var Ve=d(Qt=>{"use strict";Object.defineProperty(Qt,"__esModule",{value:!0});Qt.makeCommand=void 0;var bt=Xt(),Vo=(s,e,t,i,r)=>Object.assign((n=[],o,h)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(h=o,o=void 0),o=o?Array.from(o):[];let a=(0,bt.dealias)(n);if(r?.(a,o),(0,bt.isSyncFile)(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return s(a,o)}else if((0,bt.isAsyncFile)(a)){let l=e(a,o);return h?l.then(()=>h(),h):l}else if((0,bt.isSyncNoFile)(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return t(a,o)}else if((0,bt.isAsyncNoFile)(a)){if(typeof h=="function")throw new TypeError("callback only supported with file option");return i(a,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:e,syncNoFile:t,asyncNoFile:i,validate:r});Qt.makeCommand=Vo});var fs=d($e=>{"use strict";var $o=$e&&$e.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty($e,"__esModule",{value:!0});$e.constants=void 0;var Xo=$o(require("zlib")),Qo=Xo.default.constants||{ZLIB_VERNUM:4736};$e.constants=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},Qo))});var Ds=d(f=>{"use strict";var Jo=f&&f.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),ea=f&&f.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ta=f&&f.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;rs,ds=Wr?.writable===!0||Wr?.set!==void 0?s=>{Ae.Buffer.concat=s?oa:na}:s=>{},Ie=Symbol("_superWrite"),Fe=class extends Error{code;errno;constructor(e,t){super("zlib: "+e.message,{cause:e}),this.code=e.code,this.errno=e.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+e.message,Error.captureStackTrace(this,t??this.constructor)}get name(){return"ZlibError"}};f.ZlibError=Fe;var ms=Symbol("flushFlag"),St=class extends sa.Minipass{#e=!1;#i=!1;#s;#n;#r;#t;#o;get sawError(){return this.#e}get handle(){return this.#t}get flushFlag(){return this.#s}constructor(e,t){if(!e||typeof e!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(e),this.#s=e.flush??0,this.#n=e.finishFlush??0,this.#r=e.fullFlushFlag??0,typeof qr[t]!="function")throw new TypeError("Compression method not supported: "+t);try{this.#t=new qr[t](e)}catch(i){throw new Fe(i,this.constructor)}this.#o=i=>{this.#e||(this.#e=!0,this.close(),this.emit("error",i))},this.#t?.on("error",i=>this.#o(new Fe(i))),this.once("end",()=>this.close)}close(){this.#t&&(this.#t.close(),this.#t=void 0,this.emit("close"))}reset(){if(!this.#e)return(0,ps.default)(this.#t,"zlib binding closed"),this.#t.reset?.()}flush(e){this.ended||(typeof e!="number"&&(e=this.#r),this.write(Object.assign(Ae.Buffer.alloc(0),{[ms]:e})))}end(e,t,i){return typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&(t?this.write(e,t):this.write(e)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Ie](e){return super.write(e)}write(e,t,i){if(typeof t=="function"&&(i=t,t="utf8"),typeof e=="string"&&(e=Ae.Buffer.from(e,t)),this.#e)return;(0,ps.default)(this.#t,"zlib binding closed");let r=this.#t._handle,n=r.close;r.close=()=>{};let o=this.#t.close;this.#t.close=()=>{},ds(!0);let h;try{let l=typeof e[ms]=="number"?e[ms]:this.#s;h=this.#t._processChunk(e,l),ds(!1)}catch(l){ds(!1),this.#o(new Fe(l,this.write))}finally{this.#t&&(this.#t._handle=r,r.close=n,this.#t.close=o,this.#t.removeAllListeners("error"))}this.#t&&this.#t.on("error",l=>this.#o(new Fe(l,this.write)));let a;if(h)if(Array.isArray(h)&&h.length>0){let l=h[0];a=this[Ie](Ae.Buffer.from(l));for(let u=1;u{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(e,t)}finally{this.handle.flush=i}this.handle&&(this.#e=e,this.#i=t)}}}};f.Zlib=ie;var _s=class extends ie{constructor(e){super(e,"Deflate")}};f.Deflate=_s;var ws=class extends ie{constructor(e){super(e,"Inflate")}};f.Inflate=ws;var ys=class extends ie{#e;constructor(e){super(e,"Gzip"),this.#e=e&&!!e.portable}[Ie](e){return this.#e?(this.#e=!1,e[9]=255,super[Ie](e)):super[Ie](e)}};f.Gzip=ys;var Es=class extends ie{constructor(e){super(e,"Gunzip")}};f.Gunzip=Es;var bs=class extends ie{constructor(e){super(e,"DeflateRaw")}};f.DeflateRaw=bs;var Ss=class extends ie{constructor(e){super(e,"InflateRaw")}};f.InflateRaw=Ss;var gs=class extends ie{constructor(e){super(e,"Unzip")}};f.Unzip=gs;var Jt=class extends St{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||te.constants.BROTLI_OPERATION_FINISH,e.fullFlushFlag=te.constants.BROTLI_OPERATION_FLUSH,super(e,t)}},Os=class extends Jt{constructor(e){super(e,"BrotliCompress")}};f.BrotliCompress=Os;var Rs=class extends Jt{constructor(e){super(e,"BrotliDecompress")}};f.BrotliDecompress=Rs;var ei=class extends St{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.ZSTD_e_continue,e.finishFlush=e.finishFlush||te.constants.ZSTD_e_end,e.fullFlushFlag=te.constants.ZSTD_e_flush,super(e,t)}},vs=class extends ei{constructor(e){super(e,"ZstdCompress")}};f.ZstdCompress=vs;var Ts=class extends ei{constructor(e){super(e,"ZstdDecompress")}};f.ZstdDecompress=Ts});var Gr=d(Xe=>{"use strict";Object.defineProperty(Xe,"__esModule",{value:!0});Xe.parse=Xe.encode=void 0;var aa=(s,e)=>{if(Number.isSafeInteger(s))s<0?la(s,e):ha(s,e);else throw Error("cannot encode number outside of javascript safe integer range");return e};Xe.encode=aa;var ha=(s,e)=>{e[0]=128;for(var t=e.length;t>1;t--)e[t-1]=s&255,s=Math.floor(s/256)},la=(s,e)=>{e[0]=255;var t=!1;s=s*-1;for(var i=e.length;i>1;i--){var r=s&255;s=Math.floor(s/256),t?e[i-1]=Hr(r):r===0?e[i-1]=0:(t=!0,e[i-1]=Zr(r))}},ua=s=>{let e=s[0],t=e===128?fa(s.subarray(1,s.length)):e===255?ca(s):null;if(t===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(t))throw Error("parsed number outside of javascript safe integer range");return t};Xe.parse=ua;var ca=s=>{for(var e=s.length,t=0,i=!1,r=e-1;r>-1;r--){var n=Number(s[r]),o;i?o=Hr(n):n===0?o=n:(i=!0,o=Zr(n)),o!==0&&(t-=o*Math.pow(256,e-r-1))}return t},fa=s=>{for(var e=s.length,t=0,i=e-1;i>-1;i--){var r=Number(s[i]);r!==0&&(t+=r*Math.pow(256,e-i-1))}return t},Hr=s=>(255^s)&255,Zr=s=>(255^s)+1&255});var Ps=d(C=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0});C.code=C.name=C.normalFsTypes=C.isName=C.isCode=void 0;var da=s=>C.name.has(s);C.isCode=da;var ma=s=>C.code.has(s);C.isName=ma;C.normalFsTypes=new Set(["0","","1","2","3","4","5","6","7","D"]);C.name=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]);C.code=new Map(Array.from(C.name).map(s=>[s[1],s[0]]))});var et=d(se=>{"use strict";var pa=se&&se.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),_a=se&&se.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Yr=se&&se.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r=t+512))throw new Error("need 512 bytes for header");let n=Ce(e,t+156,1),o=Je.normalFsTypes.has(n),h=o?i:void 0,a=o?r:void 0;if(this.path=h?.path??Ce(e,t,100),this.mode=h?.mode??a?.mode??be(e,t+100,8),this.uid=h?.uid??a?.uid??be(e,t+108,8),this.gid=h?.gid??a?.gid??be(e,t+116,8),this.size=h?.size??a?.size??be(e,t+124,12),this.mtime=h?.mtime??a?.mtime??Ns(e,t+136,12),this.cksum=be(e,t+148,12),a&&this.#i(a,!0),h&&this.#i(h),Je.isCode(n)&&(this.#e=n||"0"),this.#e==="0"&&this.path.slice(-1)==="/"&&(this.#e="5"),this.#e==="5"&&(this.size=0),this.linkpath=Ce(e,t+157,100),e.subarray(t+257,t+265).toString()==="ustar\x0000")if(this.uname=h?.uname??a?.uname??Ce(e,t+265,32),this.gname=h?.gname??a?.gname??Ce(e,t+297,32),this.devmaj=h?.devmaj??a?.devmaj??be(e,t+329,8)??0,this.devmin=h?.devmin??a?.devmin??be(e,t+337,8)??0,e[t+475]!==0){let u=Ce(e,t+345,155);this.path=u+"/"+this.path}else{let u=Ce(e,t+345,130);u&&(this.path=u+"/"+this.path),this.atime=i?.atime??r?.atime??Ns(e,t+476,12),this.ctime=i?.ctime??r?.ctime??Ns(e,t+488,12)}let l=256;for(let u=t;u!(r==null||i==="path"&&t||i==="linkpath"&&t||i==="global"))))}encode(e,t=0){if(e||(e=this.block=Buffer.alloc(512)),this.#e==="Unsupported"&&(this.#e="0"),!(e.length>=t+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=wa(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=Be(e,t,100,n)||this.needPax,this.needPax=Se(e,t+100,8,this.mode)||this.needPax,this.needPax=Se(e,t+108,8,this.uid)||this.needPax,this.needPax=Se(e,t+116,8,this.gid)||this.needPax,this.needPax=Se(e,t+124,12,this.size)||this.needPax,this.needPax=Ms(e,t+136,12,this.mtime)||this.needPax,e[t+156]=Number(this.#e.codePointAt(0)),this.needPax=Be(e,t+157,100,this.linkpath)||this.needPax,e.write("ustar\x0000",t+257,8),this.needPax=Be(e,t+265,32,this.uname)||this.needPax,this.needPax=Be(e,t+297,32,this.gname)||this.needPax,this.needPax=Se(e,t+329,8,this.devmaj)||this.needPax,this.needPax=Se(e,t+337,8,this.devmin)||this.needPax,this.needPax=Be(e,t+345,i,o)||this.needPax,e[t+475]!==0?this.needPax=Be(e,t+345,155,o)||this.needPax:(this.needPax=Be(e,t+345,130,o)||this.needPax,this.needPax=Ms(e,t+476,12,this.atime)||this.needPax,this.needPax=Ms(e,t+488,12,this.ctime)||this.needPax);let h=256;for(let a=t;a{let i=s,r="",n,o=Qe.posix.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Qe.posix.dirname(i),i=Qe.posix.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=e?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=e?n=[i.slice(0,99),r,!0]:(i=Qe.posix.join(Qe.posix.basename(r),i),r=Qe.posix.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},Ce=(s,e,t)=>s.subarray(e,e+t).toString("utf8").replace(/\0.*/,""),Ns=(s,e,t)=>ya(be(s,e,t)),ya=s=>s===void 0?void 0:new Date(s*1e3),be=(s,e,t)=>Number(s[e])&128?Kr.parse(s.subarray(e,e+t)):ba(s,e,t),Ea=s=>isNaN(s)?void 0:s,ba=(s,e,t)=>Ea(parseInt(s.subarray(e,e+t).toString("utf8").replace(/\0.*$/,"").trim(),8)),Sa={12:8589934591,8:2097151},Se=(s,e,t,i)=>i===void 0?!1:i>Sa[t]||i<0?(Kr.encode(i,s.subarray(e,e+t)),!0):(ga(s,e,t,i),!1),ga=(s,e,t,i)=>s.write(Oa(i,t),e,t,"ascii"),Oa=(s,e)=>Ra(Math.floor(s).toString(8),e),Ra=(s,e)=>(s.length===e-1?s:new Array(e-s.length-1).join("0")+s+" ")+"\0",Ms=(s,e,t,i)=>i===void 0?!1:Se(s,e,t,i.getTime()/1e3),va=new Array(156).join("\0"),Be=(s,e,t,i)=>i===void 0?!1:(s.write(i+va,e,t,"utf8"),i.length!==Buffer.byteLength(i)||i.length>t)});var ii=d(ti=>{"use strict";Object.defineProperty(ti,"__esModule",{value:!0});ti.Pax=void 0;var Ta=require("node:path"),Da=et(),As=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(e,t=!1){this.atime=e.atime,this.charset=e.charset,this.comment=e.comment,this.ctime=e.ctime,this.dev=e.dev,this.gid=e.gid,this.global=t,this.gname=e.gname,this.ino=e.ino,this.linkpath=e.linkpath,this.mtime=e.mtime,this.nlink=e.nlink,this.path=e.path,this.size=e.size,this.uid=e.uid,this.uname=e.uname}encode(){let e=this.encodeBody();if(e==="")return Buffer.allocUnsafe(0);let t=Buffer.byteLength(e),i=512*Math.ceil(1+t/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new Da.Header({path:("PaxHeader/"+(0,Ta.basename)(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:t,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(e,512,t,"utf8");for(let n=t+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(e,t,i=!1){return new s(Pa(Na(e),t),i)}};ti.Pax=As;var Pa=(s,e)=>e?Object.assign({},e,s):s,Na=s=>s.replace(/\n$/,"").split(` -`).reduce(Ma,Object.create(null)),Ma=(s,e)=>{let t=parseInt(e,10);if(t!==Buffer.byteLength(e)+1)return s;e=e.slice((t+" ").length);let i=e.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=");return s[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(o)*1e3):/^[0-9]+$/.test(o)?+o:o,s}});var tt=d(si=>{"use strict";Object.defineProperty(si,"__esModule",{value:!0});si.normalizeWindowsPath=void 0;var La=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;si.normalizeWindowsPath=La!=="win32"?s=>s:s=>s&&s.replaceAll(/\\/g,"/")});var Fs=d(ni=>{"use strict";Object.defineProperty(ni,"__esModule",{value:!0});ni.ReadEntry=void 0;var Aa=We(),ri=tt(),Is=class extends Aa.Minipass{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(e,t,i){switch(super({}),this.pause(),this.extended=t,this.globalExtended=i,this.header=e,this.remain=e.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=e.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!e.path)throw new Error("no path provided for tar.ReadEntry");this.path=(0,ri.normalizeWindowsPath)(e.path),this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=this.remain,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=e.linkpath?(0,ri.normalizeWindowsPath)(e.linkpath):void 0,this.uname=e.uname,this.gname=e.gname,t&&this.#e(t),i&&this.#e(i,!0)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-t),this.blockRemain=Math.max(0,r-t),this.ignore?!0:i>=t?super.write(e):super.write(e.subarray(0,i))}#e(e,t=!1){e.path&&(e.path=(0,ri.normalizeWindowsPath)(e.path)),e.linkpath&&(e.linkpath=(0,ri.normalizeWindowsPath)(e.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(e).filter(([i,r])=>!(r==null||i==="path"&&t))))}};ni.ReadEntry=Is});var ai=d(oi=>{"use strict";Object.defineProperty(oi,"__esModule",{value:!0});oi.warnMethod=void 0;var Ia=(s,e,t,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=t instanceof Error&&t.code||e,i.tarCode=e,!s.strict&&i.recoverable!==!1?(t instanceof Error&&(i=Object.assign(t,i),t=t.message),s.emit("warn",e,t,i)):t instanceof Error?s.emit("error",Object.assign(t,i)):s.emit("error",Object.assign(new Error(`${e}: ${t}`),i))};oi.warnMethod=Ia});var pi=d(mi=>{"use strict";Object.defineProperty(mi,"__esModule",{value:!0});mi.Parser=void 0;var Fa=require("events"),Cs=Ds(),Vr=et(),$r=ii(),Ca=Fs(),Ba=ai(),za=1024*1024,js=Buffer.from([31,139]),Us=Buffer.from([40,181,47,253]),ka=Math.max(js.length,Us.length),H=Symbol("state"),ze=Symbol("writeEntry"),de=Symbol("readEntry"),Bs=Symbol("nextEntry"),Xr=Symbol("processEntry"),re=Symbol("extendedHeader"),gt=Symbol("globalExtendedHeader"),ge=Symbol("meta"),Qr=Symbol("emitMeta"),_=Symbol("buffer"),me=Symbol("queue"),Oe=Symbol("ended"),zs=Symbol("emittedEnd"),ke=Symbol("emit"),S=Symbol("unzip"),hi=Symbol("consumeChunk"),li=Symbol("consumeChunkSub"),ks=Symbol("consumeBody"),Jr=Symbol("consumeMeta"),en=Symbol("consumeHeader"),Ot=Symbol("consuming"),xs=Symbol("bufferConcat"),ui=Symbol("maybeEnd"),it=Symbol("writing"),Re=Symbol("aborted"),ci=Symbol("onDone"),xe=Symbol("sawValidEntry"),fi=Symbol("sawNullBlock"),di=Symbol("sawEOF"),tn=Symbol("closeStream"),xa=()=>!0,qs=class extends Fa.EventEmitter{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[me]=[];[_];[de];[ze];[H]="begin";[ge]="";[re];[gt];[Oe]=!1;[S];[Re]=!1;[xe];[fi]=!1;[di]=!1;[it]=!1;[Ot]=!1;[zs]=!1;constructor(e={}){super(),this.file=e.file||"",this.on(ci,()=>{(this[H]==="begin"||this[xe]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),e.ondone?this.on(ci,e.ondone):this.on(ci,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!e.strict,this.maxMetaEntrySize=e.maxMetaEntrySize||za,this.filter=typeof e.filter=="function"?e.filter:xa;let t=e.file&&(e.file.endsWith(".tar.br")||e.file.endsWith(".tbr"));this.brotli=!(e.gzip||e.zstd)&&e.brotli!==void 0?e.brotli:t?void 0:!1;let i=e.file&&(e.file.endsWith(".tar.zst")||e.file.endsWith(".tzst"));this.zstd=!(e.gzip||e.brotli)&&e.zstd!==void 0?e.zstd:i?!0:void 0,this.on("end",()=>this[tn]()),typeof e.onwarn=="function"&&this.on("warn",e.onwarn),typeof e.onReadEntry=="function"&&this.on("entry",e.onReadEntry)}warn(e,t,i={}){(0,Ba.warnMethod)(this,e,t,i)}[en](e,t){this[xe]===void 0&&(this[xe]=!1);let i;try{i=new Vr.Header(e,t,this[re],this[gt])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[fi]?(this[di]=!0,this[H]==="begin"&&(this[H]="header"),this[ke]("eof")):(this[fi]=!0,this[ke]("nullBlock"));else if(this[fi]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[ze]=new Ca.ReadEntry(i,this[re],this[gt]);if(!this[xe])if(n.remain){let o=()=>{n.invalid||(this[xe]=!0)};n.on("end",o)}else this[xe]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[ke]("ignoredEntry",n),this[H]="ignore",n.resume()):n.size>0&&(this[ge]="",n.on("data",o=>this[ge]+=o),this[H]="meta"):(this[re]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[ke]("ignoredEntry",n),this[H]=n.remain?"ignore":"header",n.resume()):(n.remain?this[H]="body":(this[H]="header",n.end()),this[de]?this[me].push(n):(this[me].push(n),this[Bs]())))}}}[tn](){queueMicrotask(()=>this.emit("close"))}[Xr](e){let t=!0;if(!e)this[de]=void 0,t=!1;else if(Array.isArray(e)){let[i,...r]=e;this.emit(i,...r)}else this[de]=e,this.emit("entry",e),e.emittedEnd||(e.on("end",()=>this[Bs]()),t=!1);return t}[Bs](){do;while(this[Xr](this[me].shift()));if(this[me].length===0){let e=this[de];!e||e.flowing||e.size===e.remain?this[it]||this.emit("drain"):e.once("drain",()=>this.emit("drain"))}}[ks](e,t){let i=this[ze];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=e.length&&t===0?e:e.subarray(t,t+r);return i.write(n),i.blockRemain||(this[H]="header",this[ze]=void 0,i.end()),n.length}[Jr](e,t){let i=this[ze],r=this[ks](e,t);return!this[ze]&&i&&this[Qr](i),r}[ke](e,t,i){this[me].length===0&&!this[de]?this.emit(e,t,i):this[me].push([e,t,i])}[Qr](e){switch(this[ke]("meta",this[ge]),e.type){case"ExtendedHeader":case"OldExtendedHeader":this[re]=$r.Pax.parse(this[ge],this[re],!1);break;case"GlobalExtendedHeader":this[gt]=$r.Pax.parse(this[ge],this[gt],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let t=this[re]??Object.create(null);this[re]=t,t.path=this[ge].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let t=this[re]||Object.create(null);this[re]=t,t.linkpath=this[ge].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+e.type)}}abort(e){this[Re]=!0,this.emit("abort",e),this.warn("TAR_ABORT",e,{recoverable:!1})}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this[Re])return i?.(),!1;if((this[S]===void 0||this.brotli===void 0&&this[S]===!1)&&e){if(this[_]&&(e=Buffer.concat([this[_],e]),this[_]=void 0),e.lengththis[hi](u)),this[S].on("error",u=>this.abort(u)),this[S].on("end",()=>{this[Oe]=!0,this[hi]()}),this[it]=!0;let l=!!this[S][a?"end":"write"](e);return this[it]=!1,i?.(),l}}this[it]=!0,this[S]?this[S].write(e):this[hi](e),this[it]=!1;let n=this[me].length>0?!1:this[de]?this[de].flowing:!0;return!n&&this[me].length===0&&this[de]?.once("drain",()=>this.emit("drain")),i?.(),n}[xs](e){e&&!this[Re]&&(this[_]=this[_]?Buffer.concat([this[_],e]):e)}[ui](){if(this[Oe]&&!this[zs]&&!this[Re]&&!this[Ot]){this[zs]=!0;let e=this[ze];if(e&&e.blockRemain){let t=this[_]?this[_].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${e.blockRemain} more bytes, only ${t} available)`,{entry:e}),this[_]&&e.write(this[_]),e.end()}this[ke](ci)}}[hi](e){if(this[Ot]&&e)this[xs](e);else if(!e&&!this[_])this[ui]();else if(e){if(this[Ot]=!0,this[_]){this[xs](e);let t=this[_];this[_]=void 0,this[li](t)}else this[li](e);for(;this[_]&&this[_]?.length>=512&&!this[Re]&&!this[di];){let t=this[_];this[_]=void 0,this[li](t)}this[Ot]=!1}(!this[_]||this[Oe])&&this[ui]()}[li](e){let t=0,i=e.length;for(;t+512<=i&&!this[Re]&&!this[di];)switch(this[H]){case"begin":case"header":this[en](e,t),t+=512;break;case"ignore":case"body":t+=this[ks](e,t);break;case"meta":t+=this[Jr](e,t);break;default:throw new Error("invalid state: "+this[H])}t{"use strict";Object.defineProperty(_i,"__esModule",{value:!0});_i.stripTrailingSlashes=void 0;var ja=s=>{let e=s.length-1,t=-1;for(;e>-1&&s.charAt(e)==="/";)t=e,e--;return t===-1?s:s.slice(0,t)};_i.stripTrailingSlashes=ja});var rt=d(B=>{"use strict";var Ua=B&&B.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),qa=B&&B.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Wa=B&&B.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=s.onReadEntry;s.onReadEntry=e?t=>{e(t),t.resume()}:t=>t.resume()},Ka=(s,e)=>{let t=new Map(e.map(n=>[(0,Ws.stripTrailingSlashes)(n),!0])),i=s.filter,r=(n,o="")=>{let h=o||(0,sn.parse)(n).root||".",a;if(n===h)a=!1;else{let l=t.get(n);a=l!==void 0?l:r((0,sn.dirname)(n),h)}return t.set(n,a),a};s.filter=i?(n,o)=>i(n,o)&&r((0,Ws.stripTrailingSlashes)(n)):n=>r((0,Ws.stripTrailingSlashes)(n))};B.filesFilter=Ka;var Va=s=>{let e=new yi.Parser(s),t=s.file,i;try{i=st.default.openSync(t,"r");let r=st.default.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let t=new yi.Parser(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{t.on("error",h),t.on("end",o),st.default.stat(r,(a,l)=>{if(a)h(a);else{let u=new Za.ReadStream(r,{readSize:i,size:l.size});u.on("error",h),u.pipe(t)}})})};B.list=(0,Ga.makeCommand)(Va,$a,s=>new yi.Parser(s),s=>new yi.Parser(s),(s,e)=>{e?.length&&(0,B.filesFilter)(s,e),s.noResume||Ya(s)})});var rn=d(Ei=>{"use strict";Object.defineProperty(Ei,"__esModule",{value:!0});Ei.modeFix=void 0;var Xa=(s,e,t)=>(s&=4095,t&&(s=(s|384)&-19),e&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);Ei.modeFix=Xa});var Hs=d(bi=>{"use strict";Object.defineProperty(bi,"__esModule",{value:!0});bi.stripAbsolutePath=void 0;var Qa=require("node:path"),{isAbsolute:Ja,parse:nn}=Qa.win32,eh=s=>{let e="",t=nn(s);for(;Ja(s)||t.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":t.root;s=s.slice(i.length),e+=i,t=nn(s)}return[e,s]};bi.stripAbsolutePath=eh});var Gs=d(nt=>{"use strict";Object.defineProperty(nt,"__esModule",{value:!0});nt.decode=nt.encode=void 0;var Si=["|","<",">","?",":"],Zs=Si.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),th=new Map(Si.map((s,e)=>[s,Zs[e]])),ih=new Map(Zs.map((s,e)=>[s,Si[e]])),sh=s=>Si.reduce((e,t)=>e.split(t).join(th.get(t)),s);nt.encode=sh;var rh=s=>Zs.reduce((e,t)=>e.split(t).join(ih.get(t)),s);nt.decode=rh});var sr=d(M=>{"use strict";var nh=M&&M.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),oh=M&&M.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ah=M&&M.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;re?(s=(0,ne.normalizeWindowsPath)(s).replace(/^\.(\/|$)/,""),(0,hh.stripTrailingSlashes)(e)+"/"+s):(0,ne.normalizeWindowsPath)(s),uh=16*1024*1024,an=Symbol("process"),hn=Symbol("file"),ln=Symbol("directory"),Ks=Symbol("symlink"),un=Symbol("hardlink"),Rt=Symbol("header"),gi=Symbol("read"),Vs=Symbol("lstat"),Oi=Symbol("onlstat"),$s=Symbol("onread"),Xs=Symbol("onreadlink"),Qs=Symbol("openfile"),Js=Symbol("onopenfile"),ve=Symbol("close"),Ri=Symbol("mode"),er=Symbol("awaitDrain"),Ys=Symbol("ondrain"),ae=Symbol("prefix"),vi=class extends fn.Minipass{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#e=!1;constructor(e,t={}){let i=(0,pn.dealias)(t);super(),this.path=(0,ne.normalizeWindowsPath)(e),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||uh,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=(0,ne.normalizeWindowsPath)(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?(0,ne.normalizeWindowsPath)(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,h]=(0,wn.stripAbsolutePath)(this.path);o&&typeof h=="string"&&(this.path=h,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=lh.decode(this.path.replaceAll(/\\/g,"/")),e=e.replaceAll(/\\/g,"/")),this.absolute=(0,ne.normalizeWindowsPath)(i.absolute||on.default.resolve(this.cwd,e)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[Oi](n):this[Vs]()}warn(e,t,i={}){return(0,yn.warnMethod)(this,e,t,i)}emit(e,...t){return e==="error"&&(this.#e=!0),super.emit(e,...t)}[Vs](){oe.default.lstat(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Oi](t)})}[Oi](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=ch(e),this.emit("stat",e),this[an]()}[an](){switch(this.type){case"File":return this[hn]();case"Directory":return this[ln]();case"SymbolicLink":return this[Ks]();default:return this.end()}}[Ri](e){return(0,mn.modeFix)(e,this.type==="Directory",this.portable)}[ae](e){return En(e,this.prefix)}[Rt](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new dn.Header({path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,mode:this[Ri](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new _n.Pax({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let e=this.header?.block;if(!e)throw new Error("failed to encode header");super.write(e)}[ln](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[Rt](),this.end()}[Ks](){oe.default.readlink(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Xs](t)})}[Xs](e){this.linkpath=(0,ne.normalizeWindowsPath)(e),this[Rt](),this.end()}[un](e){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=(0,ne.normalizeWindowsPath)(on.default.relative(this.cwd,e)),this.stat.size=0,this[Rt](),this.end()}[hn](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let e=`${this.stat.dev}:${this.stat.ino}`,t=this.linkCache.get(e);if(t?.indexOf(this.cwd)===0)return this[un](t);this.linkCache.set(e,this.absolute)}if(this[Rt](),this.stat.size===0)return this.end();this[Qs]()}[Qs](){oe.default.open(this.absolute,"r",(e,t)=>{if(e)return this.emit("error",e);this[Js](t)})}[Js](e){if(this.fd=e,this.#e)return this[ve]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let t=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(t),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[gi]()}[gi](){let{fd:e,buf:t,offset:i,length:r,pos:n}=this;if(e===void 0||t===void 0)throw new Error("cannot read file without first opening");oe.default.read(e,t,i,r,n,(o,h)=>{if(o)return this[ve](()=>this.emit("error",o));this[$s](h)})}[ve](e=()=>{}){this.fd!==void 0&&oe.default.close(this.fd,e)}[$s](e){if(e<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(e>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(e===this.remain)for(let r=e;rthis[Ys]())}[er](e){this.once("drain",e)}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this.blockRemaine?this.emit("error",e):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[gi]()}};M.WriteEntry=vi;var tr=class extends vi{sync=!0;[Vs](){this[Oi](oe.default.lstatSync(this.absolute))}[Ks](){this[Xs](oe.default.readlinkSync(this.absolute))}[Qs](){this[Js](oe.default.openSync(this.absolute,"r"))}[gi](){let e=!0;try{let{fd:t,buf:i,offset:r,length:n,pos:o}=this;if(t===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let h=oe.default.readSync(t,i,r,n,o);this[$s](h),e=!1}finally{if(e)try{this[ve](()=>{})}catch{}}}[er](e){e()}[ve](e=()=>{}){this.fd!==void 0&&oe.default.closeSync(this.fd),e()}};M.WriteEntrySync=tr;var ir=class extends fn.Minipass{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(e,t,i={}){return(0,yn.warnMethod)(this,e,t,i)}constructor(e,t={}){let i=(0,pn.dealias)(t);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=e;let{type:r}=e;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=(0,ne.normalizeWindowsPath)(e.path),this.mode=e.mode!==void 0?this[Ri](e.mode):void 0,this.uid=this.portable?void 0:e.uid,this.gid=this.portable?void 0:e.gid,this.uname=this.portable?void 0:e.uname,this.gname=this.portable?void 0:e.gname,this.size=e.size,this.mtime=this.noMtime?void 0:i.mtime||e.mtime,this.atime=this.portable?void 0:e.atime,this.ctime=this.portable?void 0:e.ctime,this.linkpath=e.linkpath!==void 0?(0,ne.normalizeWindowsPath)(e.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[h,a]=(0,wn.stripAbsolutePath)(this.path);h&&typeof a=="string"&&(this.path=a,n=h)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.onWriteEntry?.(this),this.header=new dn.Header({path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new _n.Pax({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[ae](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[ae](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),e.pipe(this)}[ae](e){return En(e,this.prefix)}[Ri](e){return(0,mn.modeFix)(e,this.type==="Directory",this.portable)}write(e,t,i){typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8"));let r=e.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(e,i)}end(e,t,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,t??"utf8")),i&&this.once("finish",i),e?super.end(e,i):super.end(i),this}};M.WriteEntryTar=ir;var ch=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported"});var bn=d(at=>{"use strict";Object.defineProperty(at,"__esModule",{value:!0});at.Node=at.Yallist=void 0;var rr=class s{tail;head;length=0;static create(e=[]){return new s(e)}constructor(e=[]){for(let t of e)this.push(t)}*[Symbol.iterator](){for(let e=this.head;e;e=e.next)yield e.value}removeNode(e){if(e.list!==this)throw new Error("removing node which does not belong to this list");let t=e.next,i=e.prev;return t&&(t.prev=i),i&&(i.next=t),e===this.head&&(this.head=t),e===this.tail&&(this.tail=i),this.length--,e.next=void 0,e.prev=void 0,e.list=void 0,t}unshiftNode(e){if(e===this.head)return;e.list&&e.list.removeNode(e);let t=this.head;e.list=this,e.next=t,t&&(t.prev=e),this.head=e,this.tail||(this.tail=e),this.length++}pushNode(e){if(e===this.tail)return;e.list&&e.list.removeNode(e);let t=this.tail;e.list=this,e.prev=t,t&&(t.next=e),this.tail=e,this.head||(this.head=e),this.length++}push(...e){for(let t=0,i=e.length;t1)i=t;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=e(i,r.value,n),r=r.next;return i}reduceReverse(e,t){let i,r=this.tail;if(arguments.length>1)i=t;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=e(i,r.value,n),r=r.prev;return i}toArray(){let e=new Array(this.length);for(let t=0,i=this.head;i;t++)e[t]=i.value,i=i.next;return e}toArrayReverse(){let e=new Array(this.length);for(let t=0,i=this.tail;i;t++)e[t]=i.value,i=i.prev;return e}slice(e=0,t=this.length){t<0&&(t+=this.length),e<0&&(e+=this.length);let i=new s;if(tthis.length&&(t=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(t=this.length);let r=this.length,n=this.tail;for(;n&&r>t;r--)n=n.prev;for(;n&&r>e;r--,n=n.prev)i.push(n.value);return i}splice(e,t=0,...i){e>this.length&&(e=this.length-1),e<0&&(e=this.length+e);let r=this.head;for(let o=0;r&&o{"use strict";var ph=L&&L.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),_h=L&&L.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),wh=L&&L.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(e.gzip&&(typeof e.gzip!="object"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new nr.Gzip(e.gzip)),e.brotli&&(typeof e.brotli!="object"&&(e.brotli={}),this.zip=new nr.BrotliCompress(e.brotli)),e.zstd&&(typeof e.zstd!="object"&&(e.zstd={}),this.zip=new nr.ZstdCompress(e.zstd)),!this.zip)throw new Error("impossible");let t=this.zip;t.on("data",i=>super.write(i)),t.on("end",()=>super.end()),t.on("drain",()=>this[hr]()),this.on("resume",()=>t.resume())}else this.on("drain",this[hr]);this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,e.mtime&&(this.mtime=e.mtime),this.filter=typeof e.filter=="function"?e.filter:()=>!0,this[X]=new Eh.Yallist,this[Q]=0,this.jobs=Number(e.jobs)||4,this[Dt]=!1,this[vt]=!1}[Tn](e){return super.write(e)}add(e){return this.write(e),this}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&this.add(e),this[vt]=!0,this[je](),i&&i(),this}write(e){if(this[vt])throw new Error("write after end");return typeof e=="string"?this[Pi](e):this[gn](e),this.flowing}[gn](e){let t=(0,lr.normalizeWindowsPath)(Rn.default.resolve(this.cwd,e.path));if(!this.filter(e.path,e))e.resume();else{let i=new Pt(e.path,t);i.entry=new ur.WriteEntryTar(e,this[ar](i)),i.entry.on("end",()=>this[or](i)),this[Q]+=1,this[X].push(i)}this[je]()}[Pi](e){let t=(0,lr.normalizeWindowsPath)(Rn.default.resolve(this.cwd,e));this[X].push(new Pt(e,t)),this[je]()}[cr](e){e.pending=!0,this[Q]+=1;let t=this.follow?"stat":"lstat";Ai.default[t](e.absolute,(i,r)=>{e.pending=!1,this[Q]-=1,i?this.emit("error",i):this[Di](e,r)})}[Di](e,t){if(this.statCache.set(e.absolute,t),e.stat=t,!this.filter(e.path,t))e.ignore=!0;else if(t.isFile()&&t.nlink>1&&!this.linkCache.get(`${t.dev}:${t.ino}`)&&!this.sync)if(e===this[Te])this[Ti](e);else{let i=`${t.dev}:${t.ino}`,r=this[Tt].get(i);r?r.push(e):this[Tt].set(i,[e]),e.pendingLink=!0,e.pending=!0}this[je]()}[fr](e){e.pending=!0,this[Q]+=1,Ai.default.readdir(e.absolute,(t,i)=>{if(e.pending=!1,this[Q]-=1,t)return this.emit("error",t);this[Ni](e,i)})}[Ni](e,t){this.readdirCache.set(e.absolute,t),e.readdir=t,this[je]()}[je](){if(!this[Dt]){this[Dt]=!0;for(let e=this[X].head;e&&this[Q]1){let i=`${t.dev}:${t.ino}`,r=this[Tt].get(i);if(r){this[Tt].delete(i);for(let n of r)n.pending=!1,this[Ti](n)}}this[je]()}[Ti](e){if(e.pending&&e.pendingLink&&e===this[Te]&&(e.pending=!1,e.pendingLink=!1),!e.pending){if(e.entry){e===this[Te]&&!e.piped&&this[Mi](e);return}if(!e.stat){let t=this.statCache.get(e.absolute);t?this[Di](e,t):this[cr](e)}if(e.stat&&!e.ignore){if(!this.noDirRecurse&&e.stat.isDirectory()&&!e.readdir){let t=this.readdirCache.get(e.absolute);if(t?this[Ni](e,t):this[fr](e),!e.readdir)return}if(e.entry=this[On](e),!e.entry){e.ignore=!0;return}e===this[Te]&&!e.piped&&this[Mi](e)}}}[ar](e){return{onwarn:(t,i,r)=>this.warn(t,i,r),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[On](e){this[Q]+=1;try{return new this[Li](e.path,this[ar](e)).on("end",()=>this[or](e)).on("error",i=>this.emit("error",i))}catch(t){this.emit("error",t)}}[hr](){this[Te]&&this[Te].entry&&this[Te].entry.resume()}[Mi](e){e.piped=!0,e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Pi](o+r)});let t=e.entry,i=this.zip;if(!t)throw new Error("cannot pipe without source");i?t.on("data",r=>{i.write(r)||t.pause()}):t.on("data",r=>{super.write(r)||t.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(e,t,i={}){(0,bh.warnMethod)(this,e,t,i)}};L.Pack=Ii;var dr=class extends Ii{sync=!0;constructor(e){super(e),this[Li]=ur.WriteEntrySync}pause(){}resume(){}[cr](e){let t=this.follow?"statSync":"lstatSync";this[Di](e,Ai.default[t](e.absolute))}[fr](e){this[Ni](e,Ai.default.readdirSync(e.absolute))}[Mi](e){let t=e.entry,i=this.zip;if(e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Pi](o+r)}),!t)throw new Error("Cannot pipe without source");i?t.on("data",r=>{i.write(r)}):t.on("data",r=>{super[Tn](r)})}};L.PackSync=dr});var mr=d(ht=>{"use strict";var Sh=ht&&ht.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ht,"__esModule",{value:!0});ht.create=void 0;var Dn=Ke(),Pn=Sh(require("node:path")),Nn=rt(),gh=Ve(),Ci=Fi(),Oh=(s,e)=>{let t=new Ci.PackSync(s),i=new Dn.WriteStreamSync(s.file,{mode:s.mode||438});t.pipe(i),Mn(t,e)},Rh=(s,e)=>{let t=new Ci.Pack(s),i=new Dn.WriteStream(s.file,{mode:s.mode||438});t.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),t.on("error",o)});return Ln(t,e).catch(n=>t.emit("error",n)),r},Mn=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,Nn.list)({file:Pn.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},Ln=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,Nn.list)({file:Pn.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(t);s.end()},vh=(s,e)=>{let t=new Ci.PackSync(s);return Mn(t,e),t},Th=(s,e)=>{let t=new Ci.Pack(s);return Ln(t,e).catch(i=>t.emit("error",i)),t};ht.create=(0,gh.makeCommand)(Oh,Rh,vh,Th,(s,e)=>{if(!e?.length)throw new TypeError("no paths specified to add to archive")})});var jn=d(lt=>{"use strict";var Dh=lt&<.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(lt,"__esModule",{value:!0});lt.getWriteFlag=void 0;var Fn=Dh(require("fs")),Ph=process.env.__FAKE_PLATFORM__||process.platform,Cn=Ph==="win32",{O_CREAT:Bn,O_NOFOLLOW:An,O_TRUNC:zn,O_WRONLY:kn}=Fn.default.constants,xn=Number(process.env.__FAKE_FS_O_FILENAME__)||Fn.default.constants.UV_FS_O_FILEMAP||0,Nh=Cn&&!!xn,Mh=512*1024,Lh=xn|zn|Bn|kn,In=!Cn&&typeof An=="number"?An|zn|Bn|kn:null;lt.getWriteFlag=In!==null?()=>In:Nh?s=>s"w"});var qn=d(he=>{"use strict";var Un=he&&he.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(he,"__esModule",{value:!0});he.chownrSync=he.chownr=void 0;var zi=Un(require("node:fs")),Nt=Un(require("node:path")),pr=(s,e,t)=>{try{return zi.default.lchownSync(s,e,t)}catch(i){if(i?.code!=="ENOENT")throw i}},Bi=(s,e,t,i)=>{zi.default.lchown(s,e,t,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},Ah=(s,e,t,i,r)=>{if(e.isDirectory())(0,he.chownr)(Nt.default.resolve(s,e.name),t,i,n=>{if(n)return r(n);let o=Nt.default.resolve(s,e.name);Bi(o,t,i,r)});else{let n=Nt.default.resolve(s,e.name);Bi(n,t,i,r)}},Ih=(s,e,t,i)=>{zi.default.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return Bi(s,e,t,i);let o=n.length,h=null,a=l=>{if(!h){if(l)return i(h=l);if(--o===0)return Bi(s,e,t,i)}};for(let l of n)Ah(s,l,e,t,a)})};he.chownr=Ih;var Fh=(s,e,t,i)=>{e.isDirectory()&&(0,he.chownrSync)(Nt.default.resolve(s,e.name),t,i),pr(Nt.default.resolve(s,e.name),t,i)},Ch=(s,e,t)=>{let i;try{i=zi.default.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return pr(s,e,t);throw n}for(let r of i)Fh(s,r,e,t);return pr(s,e,t)};he.chownrSync=Ch});var Wn=d(ki=>{"use strict";Object.defineProperty(ki,"__esModule",{value:!0});ki.CwdError=void 0;var _r=class extends Error{path;code;syscall="chdir";constructor(e,t){super(`${t}: Cannot cd into '${e}'`),this.path=e,this.code=t}get name(){return"CwdError"}};ki.CwdError=_r});var yr=d(xi=>{"use strict";Object.defineProperty(xi,"__esModule",{value:!0});xi.SymlinkError=void 0;var wr=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(e,t){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=e,this.path=t}get name(){return"SymlinkError"}};xi.SymlinkError=wr});var Kn=d(De=>{"use strict";var br=De&&De.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(De,"__esModule",{value:!0});De.mkdirSync=De.mkdir=void 0;var Hn=qn(),j=br(require("node:fs")),Bh=br(require("node:fs/promises")),ji=br(require("node:path")),Zn=Wn(),pe=tt(),Gn=yr(),zh=(s,e)=>{j.default.stat(s,(t,i)=>{(t||!i.isDirectory())&&(t=new Zn.CwdError(s,t?.code||"ENOTDIR")),e(t)})},kh=(s,e,t)=>{s=(0,pe.normalizeWindowsPath)(s);let i=e.umask??18,r=e.mode|448,n=(r&i)!==0,o=e.uid,h=e.gid,a=typeof o=="number"&&typeof h=="number"&&(o!==e.processUid||h!==e.processGid),l=e.preserve,u=e.unlink,c=(0,pe.normalizeWindowsPath)(e.cwd),E=(w,P)=>{w?t(w):P&&a?(0,Hn.chownr)(P,o,h,kt=>E(kt)):n?j.default.chmod(s,r,t):t()};if(s===c)return zh(s,E);if(l)return Bh.default.mkdir(s,{mode:r,recursive:!0}).then(w=>E(null,w??void 0),E);let A=(0,pe.normalizeWindowsPath)(ji.default.relative(c,s)).split("/");Er(c,A,r,u,c,void 0,E)};De.mkdir=kh;var Er=(s,e,t,i,r,n,o)=>{if(e.length===0)return o(null,n);let h=e.shift(),a=(0,pe.normalizeWindowsPath)(ji.default.resolve(s+"/"+h));j.default.mkdir(a,t,Yn(a,e,t,i,r,n,o))},Yn=(s,e,t,i,r,n,o)=>h=>{h?j.default.lstat(s,(a,l)=>{if(a)a.path=a.path&&(0,pe.normalizeWindowsPath)(a.path),o(a);else if(l.isDirectory())Er(s,e,t,i,r,n,o);else if(i)j.default.unlink(s,u=>{if(u)return o(u);j.default.mkdir(s,t,Yn(s,e,t,i,r,n,o))});else{if(l.isSymbolicLink())return o(new Gn.SymlinkError(s,s+"/"+e.join("/")));o(h)}}):(n=n||s,Er(s,e,t,i,r,n,o))},xh=s=>{let e=!1,t;try{e=j.default.statSync(s).isDirectory()}catch(i){t=i?.code}finally{if(!e)throw new Zn.CwdError(s,t??"ENOTDIR")}},jh=(s,e)=>{s=(0,pe.normalizeWindowsPath)(s);let t=e.umask??18,i=e.mode|448,r=(i&t)!==0,n=e.uid,o=e.gid,h=typeof n=="number"&&typeof o=="number"&&(n!==e.processUid||o!==e.processGid),a=e.preserve,l=e.unlink,u=(0,pe.normalizeWindowsPath)(e.cwd),c=w=>{w&&h&&(0,Hn.chownrSync)(w,n,o),r&&j.default.chmodSync(s,i)};if(s===u)return xh(u),c();if(a)return c(j.default.mkdirSync(s,{mode:i,recursive:!0})??void 0);let D=(0,pe.normalizeWindowsPath)(ji.default.relative(u,s)).split("/"),A;for(let w=D.shift(),P=u;w&&(P+="/"+w);w=D.shift()){P=(0,pe.normalizeWindowsPath)(ji.default.resolve(P));try{j.default.mkdirSync(P,i),A=A||P}catch{let kt=j.default.lstatSync(P);if(kt.isDirectory())continue;if(l){j.default.unlinkSync(P),j.default.mkdirSync(P,i),A=A||P;continue}else if(kt.isSymbolicLink())return new Gn.SymlinkError(P,P+"/"+D.join("/"))}}return c(A)};De.mkdirSync=jh});var $n=d(Ui=>{"use strict";Object.defineProperty(Ui,"__esModule",{value:!0});Ui.normalizeUnicode=void 0;var Sr=Object.create(null),Vn=1e4,ut=new Set,Uh=s=>{ut.has(s)?ut.delete(s):Sr[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),ut.add(s);let e=Sr[s],t=ut.size-Vn;if(t>Vn/10){for(let i of ut)if(ut.delete(i),delete Sr[i],--t<=0)break}return e};Ui.normalizeUnicode=Uh});var Qn=d(qi=>{"use strict";Object.defineProperty(qi,"__esModule",{value:!0});qi.PathReservations=void 0;var Xn=require("node:path"),qh=$n(),Wh=wi(),Hh=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Zh=Hh==="win32",Gh=s=>s.split("/").slice(0,-1).reduce((t,i)=>{let r=t.at(-1);return r!==void 0&&(i=(0,Xn.join)(r,i)),t.push(i||"/"),t},[]),gr=class{#e=new Map;#i=new Map;#s=new Set;reserve(e,t){e=Zh?["win32 parallelization disabled"]:e.map(r=>(0,Wh.stripTrailingSlashes)((0,Xn.join)((0,qh.normalizeUnicode)(r))));let i=new Set(e.map(r=>Gh(r)).reduce((r,n)=>r.concat(n)));this.#i.set(t,{dirs:i,paths:e});for(let r of e){let n=this.#e.get(r);n?n.push(t):this.#e.set(r,[t])}for(let r of i){let n=this.#e.get(r);if(!n)this.#e.set(r,[new Set([t])]);else{let o=n.at(-1);o instanceof Set?o.add(t):n.push(new Set([t]))}}return this.#r(t)}#n(e){let t=this.#i.get(e);if(!t)throw new Error("function does not have any path reservations");return{paths:t.paths.map(i=>this.#e.get(i)),dirs:[...t.dirs].map(i=>this.#e.get(i))}}check(e){let{paths:t,dirs:i}=this.#n(e);return t.every(r=>r&&r[0]===e)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(e))}#r(e){return this.#s.has(e)||!this.check(e)?!1:(this.#s.add(e),e(()=>this.#t(e)),!0)}#t(e){if(!this.#s.has(e))return!1;let t=this.#i.get(e);if(!t)throw new Error("invalid reservation");let{paths:i,dirs:r}=t,n=new Set;for(let o of i){let h=this.#e.get(o);if(!h||h?.[0]!==e)continue;let a=h[1];if(!a){this.#e.delete(o);continue}if(h.shift(),typeof a=="function")n.add(a);else for(let l of a)n.add(l)}for(let o of r){let h=this.#e.get(o),a=h?.[0];if(!(!h||!(a instanceof Set)))if(a.size===1&&h.length===1){this.#e.delete(o);continue}else if(a.size===1){h.shift();let l=h[0];typeof l=="function"&&n.add(l)}else a.delete(e)}return this.#s.delete(e),n.forEach(o=>this.#r(o)),!0}};qi.PathReservations=gr});var Jn=d(Wi=>{"use strict";Object.defineProperty(Wi,"__esModule",{value:!0});Wi.umask=void 0;var Yh=()=>process.umask();Wi.umask=Yh});var Ir=d(k=>{"use strict";var Kh=k&&k.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Vh=k&&k.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),lo=k&&k.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{if(!Bt)return m.default.unlink(s,e);let t=s+".DELETE."+(0,uo.randomBytes)(16).toString("hex");m.default.rename(s,t,i=>{if(i)return e(i);m.default.unlink(t,e)})},nl=s=>{if(!Bt)return m.default.unlinkSync(s);let e=s+".DELETE."+(0,uo.randomBytes)(16).toString("hex");m.default.renameSync(s,e),m.default.unlinkSync(e)},ho=(s,e,t)=>s!==void 0&&s===s>>>0?s:e!==void 0&&e===e>>>0?e:t,Gi=class extends Qh.Parser{[Rr]=!1;[Ct]=!1;[Hi]=0;reservations=new el.PathReservations;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(e={}){if(e.ondone=()=>{this[Rr]=!0,this[vr]()},super(e),this.transform=e.transform,this.chmod=!!e.chmod,typeof e.uid=="number"||typeof e.gid=="number"){if(typeof e.uid!="number"||typeof e.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(e.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=e.preserveOwner===void 0&&typeof e.uid!="number"?!!(process.getuid&&process.getuid()===0):!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof e.maxDepth=="number"?e.maxDepth:sl,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||Bt,this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=(0,U.normalizeWindowsPath)(g.default.resolve(e.cwd||process.cwd())),this.strip=Number(e.strip)||0,this.processUmask=this.chmod?typeof e.processUmask=="number"?e.processUmask:(0,tl.umask)():0,this.umask=typeof e.umask=="number"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on("entry",t=>this[to](t))}warn(e,t,i={}){return(e==="TAR_BAD_ARCHIVE"||e==="TAR_ABORT")&&(i.recoverable=!1),super.warn(e,t,i)}[vr](){this[Rr]&&this[Hi]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[Or](e,t){let i=e[t],{type:r}=e;if(!i||this.preservePaths)return!0;let[n,o]=(0,Jh.stripAbsolutePath)(i),h=o.replaceAll(/\\/g,"/").split("/");if(h.includes("..")||Bt&&/^[a-z]:\.\.$/i.test(h[0]??"")){if(t==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${t} contains '..'`,{entry:e,[t]:i}),!1;let a=g.default.posix.dirname(e.path),l=g.default.posix.normalize(g.default.posix.join(a,h.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${t} escapes extraction directory`,{entry:e,[t]:i}),!1}return n&&(e[t]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${t}`,{entry:e,[t]:i})),!0}[oo](e){let t=(0,U.normalizeWindowsPath)(e.path),i=t.split("/");if(this.strip){if(i.length=this.strip)e.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),e.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:e,path:t,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[Or](e,"path")||!this[Or](e,"linkpath"))return!1;if(e.absolute=g.default.isAbsolute(e.path)?(0,U.normalizeWindowsPath)(g.default.resolve(e.path)):(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,e.path)),!this.preservePaths&&typeof e.absolute=="string"&&e.absolute.indexOf(this.cwd+"/")!==0&&e.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:e,path:(0,U.normalizeWindowsPath)(e.path),resolvedPath:e.absolute,cwd:this.cwd}),!1;if(e.absolute===this.cwd&&e.type!=="Directory"&&e.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=g.default.win32.parse(String(e.absolute));e.absolute=r+eo.encode(String(e.absolute).slice(r.length));let{root:n}=g.default.win32.parse(e.path);e.path=n+eo.encode(e.path.slice(n.length))}return!0}[to](e){if(!this[oo](e))return e.resume();switch(Xh.default.equal(typeof e.absolute,"string"),e.type){case"Directory":case"GNUDumpDir":e.mode&&(e.mode=e.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[Tr](e);default:return this[no](e)}}[T](e,t){e.name==="CwdError"?this.emit("error",e):(this.warn("TAR_ENTRY_ERROR",e,{entry:t}),this[ct](),t.resume())}[Pe](e,t,i){(0,fo.mkdir)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t},i)}[At](e){return this.forceChown||this.preserveOwner&&(typeof e.uid=="number"&&e.uid!==this.processUid||typeof e.gid=="number"&&e.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[It](e){return ho(this.uid,e.uid,this.processUid)}[Ft](e){return ho(this.gid,e.gid,this.processGid)}[Pr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=new $h.WriteStream(String(e.absolute),{flags:(0,co.getWriteFlag)(e.size),mode:i,autoClose:!1});r.on("error",a=>{r.fd&&m.default.close(r.fd,()=>{}),r.write=()=>!0,this[T](a,e),t()});let n=1,o=a=>{if(a){r.fd&&m.default.close(r.fd,()=>{}),this[T](a,e),t();return}--n===0&&r.fd!==void 0&&m.default.close(r.fd,l=>{l?this[T](l,e):this[ct](),t()})};r.on("finish",()=>{let a=String(e.absolute),l=r.fd;if(typeof l=="number"&&e.mtime&&!this.noMtime){n++;let u=e.atime||new Date,c=e.mtime;m.default.futimes(l,u,c,E=>E?m.default.utimes(a,u,c,D=>o(D&&E)):o())}if(typeof l=="number"&&this[At](e)){n++;let u=this[It](e),c=this[Ft](e);typeof u=="number"&&typeof c=="number"&&m.default.fchown(l,u,c,E=>E?m.default.chown(a,u,c,D=>o(D&&E)):o())}o()});let h=this.transform&&this.transform(e)||e;h!==e&&(h.on("error",a=>{this[T](a,e),t()}),e.pipe(h)),h.pipe(r)}[Nr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode;this[Pe](String(e.absolute),i,r=>{if(r){this[T](r,e),t();return}let n=1,o=()=>{--n===0&&(t(),this[ct](),e.resume())};e.mtime&&!this.noMtime&&(n++,m.default.utimes(String(e.absolute),e.atime||new Date,e.mtime,o)),this[At](e)&&(n++,m.default.chown(String(e.absolute),Number(this[It](e)),Number(this[Ft](e)),o)),o()})}[no](e){e.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[so](e,t){let i=(0,U.normalizeWindowsPath)(g.default.relative(this.cwd,g.default.resolve(g.default.dirname(String(e.absolute)),String(e.linkpath)))).split("/");this[Lt](e,this.cwd,i,()=>this[Zi](e,String(e.linkpath),"symlink",t),r=>{this[T](r,e),t()})}[ro](e,t){let i=(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,String(e.linkpath))),r=(0,U.normalizeWindowsPath)(String(e.linkpath)).split("/");this[Lt](e,this.cwd,r,()=>this[Zi](e,i,"link",t),n=>{this[T](n,e),t()})}[Lt](e,t,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let h=g.default.resolve(t,o);m.default.lstat(h,(a,l)=>{if(a)return r();if(l?.isSymbolicLink())return n(new mo.SymlinkError(h,g.default.resolve(h,i.join("/"))));this[Lt](e,h,i,r,n)})}[ao](){this[Hi]++}[ct](){this[Hi]--,this[vr]()}[Mr](e){this[ct](),e.resume()}[Dr](e,t){return e.type==="File"&&!this.unlink&&t.isFile()&&t.nlink<=1&&!Bt}[Tr](e){this[ao]();let t=[e.path];e.linkpath&&t.push(e.linkpath),this.reservations.reserve(t,i=>this[io](e,i))}[io](e,t){let i=h=>{t(h)},r=()=>{this[Pe](this.cwd,this.dmode,h=>{if(h){this[T](h,e),i();return}this[Ct]=!0,n()})},n=()=>{if(e.absolute!==this.cwd){let h=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(h!==this.cwd)return this[Pe](h,this.dmode,a=>{if(a){this[T](a,e),i();return}o()})}o()},o=()=>{m.default.lstat(String(e.absolute),(h,a)=>{if(a&&(this.keep||this.newer&&a.mtime>(e.mtime??a.mtime))){this[Mr](e),i();return}if(h||this[Dr](e,a))return this[Z](null,e,i);if(a.isDirectory()){if(e.type==="Directory"){let l=this.chmod&&e.mode&&(a.mode&4095)!==e.mode,u=c=>this[Z](c??null,e,i);return l?m.default.chmod(String(e.absolute),Number(e.mode),u):u()}if(e.absolute!==this.cwd)return m.default.rmdir(String(e.absolute),l=>this[Z](l??null,e,i))}if(e.absolute===this.cwd)return this[Z](null,e,i);rl(String(e.absolute),l=>this[Z](l??null,e,i))})};this[Ct]?n():r()}[Z](e,t,i){if(e){this[T](e,t),i();return}switch(t.type){case"File":case"OldFile":case"ContiguousFile":return this[Pr](t,i);case"Link":return this[ro](t,i);case"SymbolicLink":return this[so](t,i);case"Directory":case"GNUDumpDir":return this[Nr](t,i)}}[Zi](e,t,i,r){m.default[i](t,String(e.absolute),n=>{n?this[T](n,e):(this[ct](),e.resume()),r()})}};k.Unpack=Gi;var Mt=s=>{try{return[null,s()]}catch(e){return[e,null]}},Lr=class extends Gi{sync=!0;[Z](e,t){return super[Z](e,t,()=>{})}[Tr](e){if(!this[Ct]){let n=this[Pe](this.cwd,this.dmode);if(n)return this[T](n,e);this[Ct]=!0}if(e.absolute!==this.cwd){let n=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(n!==this.cwd){let o=this[Pe](n,this.dmode);if(o)return this[T](o,e)}}let[t,i]=Mt(()=>m.default.lstatSync(String(e.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(e.mtime??i.mtime)))return this[Mr](e);if(t||this[Dr](e,i))return this[Z](null,e);if(i.isDirectory()){if(e.type==="Directory"){let o=this.chmod&&e.mode&&(i.mode&4095)!==e.mode,[h]=o?Mt(()=>{m.default.chmodSync(String(e.absolute),Number(e.mode))}):[];return this[Z](h,e)}let[n]=Mt(()=>m.default.rmdirSync(String(e.absolute)));this[Z](n,e)}let[r]=e.absolute===this.cwd?[]:Mt(()=>nl(String(e.absolute)));this[Z](r,e)}[Pr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=h=>{let a;try{m.default.closeSync(n)}catch(l){a=l}(h||a)&&this[T](h||a,e),t()},n;try{n=m.default.openSync(String(e.absolute),(0,co.getWriteFlag)(e.size),i)}catch(h){return r(h)}let o=this.transform&&this.transform(e)||e;o!==e&&(o.on("error",h=>this[T](h,e)),e.pipe(o)),o.on("data",h=>{try{m.default.writeSync(n,h,0,h.length)}catch(a){r(a)}}),o.on("end",()=>{let h=null;if(e.mtime&&!this.noMtime){let a=e.atime||new Date,l=e.mtime;try{m.default.futimesSync(n,a,l)}catch(u){try{m.default.utimesSync(String(e.absolute),a,l)}catch{h=u}}}if(this[At](e)){let a=this[It](e),l=this[Ft](e);try{m.default.fchownSync(n,Number(a),Number(l))}catch(u){try{m.default.chownSync(String(e.absolute),Number(a),Number(l))}catch{h=h||u}}}r(h)})}[Nr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode,r=this[Pe](String(e.absolute),i);if(r){this[T](r,e),t();return}if(e.mtime&&!this.noMtime)try{m.default.utimesSync(String(e.absolute),e.atime||new Date,e.mtime)}catch{}if(this[At](e))try{m.default.chownSync(String(e.absolute),Number(this[It](e)),Number(this[Ft](e)))}catch{}t(),e.resume()}[Pe](e,t){try{return(0,fo.mkdirSync)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t})}catch(i){return i}}[Lt](e,t,i,r,n){if(this.preservePaths||i.length===0)return r();let o=t;for(let h of i){o=g.default.resolve(o,h);let[a,l]=Mt(()=>m.default.lstatSync(o));if(a)return r();if(l.isSymbolicLink())return n(new mo.SymlinkError(o,g.default.resolve(t,i.join("/"))))}r()}[Zi](e,t,i,r){let n=`${i}Sync`;try{m.default[n](t,String(e.absolute)),r(),e.resume()}catch(o){return this[T](o,e)}}};k.UnpackSync=Lr});var Fr=d(G=>{"use strict";var ol=G&&G.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),al=G&&G.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),hl=G&&G.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=new Yi.UnpackSync(s),t=s.file,i=_o.default.statSync(t),r=s.maxReadSize||16*1024*1024;new po.ReadStreamSync(t,{readSize:r,size:i.size}).pipe(e)},dl=(s,e)=>{let t=new Yi.Unpack(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{t.on("error",h),t.on("close",o),_o.default.stat(r,(a,l)=>{if(a)h(a);else{let u=new po.ReadStream(r,{readSize:i,size:l.size});u.on("error",h),u.pipe(t)}})})};G.extract=(0,cl.makeCommand)(fl,dl,s=>new Yi.UnpackSync(s),s=>new Yi.Unpack(s),(s,e)=>{e?.length&&(0,ul.filesFilter)(s,e)})});var Ki=d(ft=>{"use strict";var wo=ft&&ft.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ft,"__esModule",{value:!0});ft.replace=void 0;var yo=Ke(),q=wo(require("node:fs")),Eo=wo(require("node:path")),bo=et(),So=rt(),ml=Ve(),pl=Xt(),go=Fi(),_l=(s,e)=>{let t=new go.PackSync(s),i=!0,r,n;try{try{r=q.default.openSync(s.file,"r+")}catch(a){if(a?.code==="ENOENT")r=q.default.openSync(s.file,"w+");else throw a}let o=q.default.fstatSync(r),h=Buffer.alloc(512);e:for(n=0;no.size)break;n+=l,s.mtimeCache&&a.mtime&&s.mtimeCache.set(String(a.path),a.mtime)}i=!1,wl(s,t,n,r,e)}finally{if(i)try{q.default.closeSync(r)}catch{}}},wl=(s,e,t,i,r)=>{let n=new yo.WriteStreamSync(s.file,{fd:i,start:t});e.pipe(n),El(e,r)},yl=(s,e)=>{e=Array.from(e);let t=new go.Pack(s),i=(n,o,h)=>{let a=(D,A)=>{D?q.default.close(n,w=>h(D)):h(null,A)},l=0;if(o===0)return a(null,0);let u=0,c=Buffer.alloc(512),E=(D,A)=>{if(D||A===void 0)return a(D);if(u+=A,u<512&&A)return q.default.read(n,c,u,c.length-u,l+u,E);if(l===0&&c[0]===31&&c[1]===139)return a(new Error("cannot append to compressed archives"));if(u<512)return a(null,l);let w=new bo.Header(c);if(!w.cksumValid)return a(null,l);let P=512*Math.ceil((w.size??0)/512);if(l+P+512>o||(l+=P+512,l>=o))return a(null,l);s.mtimeCache&&w.mtime&&s.mtimeCache.set(String(w.path),w.mtime),u=0,q.default.read(n,c,0,512,l,E)};q.default.read(n,c,0,512,l,E)};return new Promise((n,o)=>{t.on("error",o);let h="r+",a=(l,u)=>{if(l&&l.code==="ENOENT"&&h==="r+")return h="w+",q.default.open(s.file,h,a);if(l||!u)return o(l);q.default.fstat(u,(c,E)=>{if(c)return q.default.close(u,()=>o(c));i(u,E.size,(D,A)=>{if(D)return o(D);let w=new yo.WriteStream(s.file,{fd:u,start:A});t.pipe(w),w.on("error",o),w.on("close",n),bl(t,e)})})};q.default.open(s.file,h,a)})},El=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,So.list)({file:Eo.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},bl=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,So.list)({file:Eo.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t);s.end()};ft.replace=(0,ml.makeCommand)(_l,yl,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,e)=>{if(!(0,pl.isFile)(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!e?.length)throw new TypeError("no paths specified to add/replace")})});var Cr=d(Vi=>{"use strict";Object.defineProperty(Vi,"__esModule",{value:!0});Vi.update=void 0;var Sl=Ve(),zt=Ki();Vi.update=(0,Sl.makeCommand)(zt.replace.syncFile,zt.replace.asyncFile,zt.replace.syncNoFile,zt.replace.asyncNoFile,(s,e=[])=>{zt.replace.validate?.(s,e),gl(s)});var gl=s=>{let e=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=e?(t,i)=>e(t,i)&&!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0)):(t,i)=>!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0))}});var Oo=exports&&exports.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Ol=exports&&exports.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Y=exports&&exports.__exportStar||function(s,e){for(var t in s)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&Oo(e,s,t)},Rl=exports&&exports.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r()=>(e||s((e={exports:{}}).exports,e),e.exports);var We=d(F=>{"use strict";var Do=F&&F.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(F,"__esModule",{value:!0});F.Minipass=F.isWritable=F.isReadable=F.isStream=void 0;var kr=typeof process=="object"&&process?process:{stdout:null,stderr:null},ss=require("node:events"),qr=Do(require("node:stream")),Po=require("node:string_decoder"),No=s=>!!s&&typeof s=="object"&&(s instanceof Zt||s instanceof qr.default||(0,F.isReadable)(s)||(0,F.isWritable)(s));F.isStream=No;var Mo=s=>!!s&&typeof s=="object"&&s instanceof ss.EventEmitter&&typeof s.pipe=="function"&&s.pipe!==qr.default.Writable.prototype.pipe;F.isReadable=Mo;var Lo=s=>!!s&&typeof s=="object"&&s instanceof ss.EventEmitter&&typeof s.write=="function"&&typeof s.end=="function";F.isWritable=Lo;var ce=Symbol("EOF"),ue=Symbol("maybeEmitEnd"),we=Symbol("emittedEnd"),jt=Symbol("emittingEnd"),dt=Symbol("emittedError"),Ut=Symbol("closed"),xr=Symbol("read"),qt=Symbol("flush"),jr=Symbol("flushChunk"),K=Symbol("encoding"),Ue=Symbol("decoder"),R=Symbol("flowing"),mt=Symbol("paused"),qe=Symbol("resume"),O=Symbol("buffer"),I=Symbol("pipes"),v=Symbol("bufferLength"),Xi=Symbol("bufferPush"),Wt=Symbol("bufferShift"),N=Symbol("objectMode"),y=Symbol("destroyed"),Qi=Symbol("error"),Ji=Symbol("emitData"),Ur=Symbol("emitEnd"),es=Symbol("emitEnd2"),J=Symbol("async"),ts=Symbol("abort"),Ht=Symbol("aborted"),pt=Symbol("signal"),Ne=Symbol("dataListeners"),x=Symbol("discarded"),_t=s=>Promise.resolve().then(s),Ao=s=>s(),Io=s=>s==="end"||s==="finish"||s==="prefinish",Fo=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,Co=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),Gt=class{src;dest;opts;ondrain;constructor(e,t,i){this.src=e,this.dest=t,this.opts=i,this.ondrain=()=>e[qe](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(e){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},is=class extends Gt{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(e,t,i){super(e,t,i),this.proxyErrors=r=>this.dest.emit("error",r),e.on("error",this.proxyErrors)}},Bo=s=>!!s.objectMode,zo=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",Zt=class extends ss.EventEmitter{[R]=!1;[mt]=!1;[I]=[];[O]=[];[N];[K];[J];[Ue];[ce]=!1;[we]=!1;[jt]=!1;[Ut]=!1;[dt]=null;[v]=0;[y]=!1;[pt];[Ht]=!1;[Ne]=0;[x]=!1;writable=!0;readable=!0;constructor(...e){let t=e[0]||{};if(super(),t.objectMode&&typeof t.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Bo(t)?(this[N]=!0,this[K]=null):zo(t)?(this[K]=t.encoding,this[N]=!1):(this[N]=!1,this[K]=null),this[J]=!!t.async,this[Ue]=this[K]?new Po.StringDecoder(this[K]):null,t&&t.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[O]}),t&&t.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[I]});let{signal:i}=t;i&&(this[pt]=i,i.aborted?this[ts]():i.addEventListener("abort",()=>this[ts]()))}get bufferLength(){return this[v]}get encoding(){return this[K]}set encoding(e){throw new Error("Encoding must be set at instantiation time")}setEncoding(e){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[N]}set objectMode(e){throw new Error("objectMode must be set at instantiation time")}get async(){return this[J]}set async(e){this[J]=this[J]||!!e}[ts](){this[Ht]=!0,this.emit("abort",this[pt]?.reason),this.destroy(this[pt]?.reason)}get aborted(){return this[Ht]}set aborted(e){}write(e,t,i){if(this[Ht])return!1;if(this[ce])throw new Error("write after end");if(this[y])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof t=="function"&&(i=t,t="utf8"),t||(t="utf8");let r=this[J]?_t:Ao;if(!this[N]&&!Buffer.isBuffer(e)){if(Co(e))e=Buffer.from(e.buffer,e.byteOffset,e.byteLength);else if(Fo(e))e=Buffer.from(e);else if(typeof e!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[N]?(this[R]&&this[v]!==0&&this[qt](!0),this[R]?this.emit("data",e):this[Xi](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[R]):e.length?(typeof e=="string"&&!(t===this[K]&&!this[Ue]?.lastNeed)&&(e=Buffer.from(e,t)),Buffer.isBuffer(e)&&this[K]&&(e=this[Ue].write(e)),this[R]&&this[v]!==0&&this[qt](!0),this[R]?this.emit("data",e):this[Xi](e),this[v]!==0&&this.emit("readable"),i&&r(i),this[R]):(this[v]!==0&&this.emit("readable"),i&&r(i),this[R])}read(e){if(this[y])return null;if(this[x]=!1,this[v]===0||e===0||e&&e>this[v])return this[ue](),null;this[N]&&(e=null),this[O].length>1&&!this[N]&&(this[O]=[this[K]?this[O].join(""):Buffer.concat(this[O],this[v])]);let t=this[xr](e||null,this[O][0]);return this[ue](),t}[xr](e,t){if(this[N])this[Wt]();else{let i=t;e===i.length||e===null?this[Wt]():typeof i=="string"?(this[O][0]=i.slice(e),t=i.slice(0,e),this[v]-=e):(this[O][0]=i.subarray(e),t=i.subarray(0,e),this[v]-=e)}return this.emit("data",t),!this[O].length&&!this[ce]&&this.emit("drain"),t}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t="utf8"),e!==void 0&&this.write(e,t),i&&this.once("end",i),this[ce]=!0,this.writable=!1,(this[R]||!this[mt])&&this[ue](),this}[qe](){this[y]||(!this[Ne]&&!this[I].length&&(this[x]=!0),this[mt]=!1,this[R]=!0,this.emit("resume"),this[O].length?this[qt]():this[ce]?this[ue]():this.emit("drain"))}resume(){return this[qe]()}pause(){this[R]=!1,this[mt]=!0,this[x]=!1}get destroyed(){return this[y]}get flowing(){return this[R]}get paused(){return this[mt]}[Xi](e){this[N]?this[v]+=1:this[v]+=e.length,this[O].push(e)}[Wt](){return this[N]?this[v]-=1:this[v]-=this[O][0].length,this[O].shift()}[qt](e=!1){do;while(this[jr](this[Wt]())&&this[O].length);!e&&!this[O].length&&!this[ce]&&this.emit("drain")}[jr](e){return this.emit("data",e),this[R]}pipe(e,t){if(this[y])return e;this[x]=!1;let i=this[we];return t=t||{},e===kr.stdout||e===kr.stderr?t.end=!1:t.end=t.end!==!1,t.proxyErrors=!!t.proxyErrors,i?t.end&&e.end():(this[I].push(t.proxyErrors?new is(this,e,t):new Gt(this,e,t)),this[J]?_t(()=>this[qe]()):this[qe]()),e}unpipe(e){let t=this[I].find(i=>i.dest===e);t&&(this[I].length===1?(this[R]&&this[Ne]===0&&(this[R]=!1),this[I]=[]):this[I].splice(this[I].indexOf(t),1),t.unpipe())}addListener(e,t){return this.on(e,t)}on(e,t){let i=super.on(e,t);if(e==="data")this[x]=!1,this[Ne]++,!this[I].length&&!this[R]&&this[qe]();else if(e==="readable"&&this[v]!==0)super.emit("readable");else if(Io(e)&&this[we])super.emit(e),this.removeAllListeners(e);else if(e==="error"&&this[dt]){let r=t;this[J]?_t(()=>r.call(this,this[dt])):r.call(this,this[dt])}return i}removeListener(e,t){return this.off(e,t)}off(e,t){let i=super.off(e,t);return e==="data"&&(this[Ne]=this.listeners("data").length,this[Ne]===0&&!this[x]&&!this[I].length&&(this[R]=!1)),i}removeAllListeners(e){let t=super.removeAllListeners(e);return(e==="data"||e===void 0)&&(this[Ne]=0,!this[x]&&!this[I].length&&(this[R]=!1)),t}get emittedEnd(){return this[we]}[ue](){!this[jt]&&!this[we]&&!this[y]&&this[O].length===0&&this[ce]&&(this[jt]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[Ut]&&this.emit("close"),this[jt]=!1)}emit(e,...t){let i=t[0];if(e!=="error"&&e!=="close"&&e!==y&&this[y])return!1;if(e==="data")return!this[N]&&!i?!1:this[J]?(_t(()=>this[Ji](i)),!0):this[Ji](i);if(e==="end")return this[Ur]();if(e==="close"){if(this[Ut]=!0,!this[we]&&!this[y])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(e==="error"){this[dt]=i,super.emit(Qi,i);let n=!this[pt]||this.listeners("error").length?super.emit("error",i):!1;return this[ue](),n}else if(e==="resume"){let n=super.emit("resume");return this[ue](),n}else if(e==="finish"||e==="prefinish"){let n=super.emit(e);return this.removeAllListeners(e),n}let r=super.emit(e,...t);return this[ue](),r}[Ji](e){for(let i of this[I])i.dest.write(e)===!1&&this.pause();let t=this[x]?!1:super.emit("data",e);return this[ue](),t}[Ur](){return this[we]?!1:(this[we]=!0,this.readable=!1,this[J]?(_t(()=>this[es]()),!0):this[es]())}[es](){if(this[Ue]){let t=this[Ue].end();if(t){for(let i of this[I])i.dest.write(t);this[x]||super.emit("data",t)}}for(let t of this[I])t.end();let e=super.emit("end");return this.removeAllListeners("end"),e}async collect(){let e=Object.assign([],{dataLength:0});this[N]||(e.dataLength=0);let t=this.promise();return this.on("data",i=>{e.push(i),this[N]||(e.dataLength+=i.length)}),await t,e}async concat(){if(this[N])throw new Error("cannot concat in objectMode");let e=await this.collect();return this[K]?e.join(""):Buffer.concat(e,e.dataLength)}async promise(){return new Promise((e,t)=>{this.on(y,()=>t(new Error("stream destroyed"))),this.on("error",i=>t(i)),this.on("end",()=>e())})}[Symbol.asyncIterator](){this[x]=!1;let e=!1,t=async()=>(this.pause(),e=!0,{value:void 0,done:!0});return{next:()=>{if(e)return t();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[ce])return t();let n,o,a=u=>{this.off("data",h),this.off("end",l),this.off(y,c),t(),o(u)},h=u=>{this.off("error",a),this.off("end",l),this.off(y,c),this.pause(),n({value:u,done:!!this[ce]})},l=()=>{this.off("error",a),this.off("data",h),this.off(y,c),t(),n({done:!0,value:void 0})},c=()=>a(new Error("stream destroyed"));return new Promise((u,E)=>{o=E,n=u,this.once(y,c),this.once("error",a),this.once("end",l),this.once("data",h)})},throw:t,return:t,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[x]=!1;let e=!1,t=()=>(this.pause(),this.off(Qi,t),this.off(y,t),this.off("end",t),e=!0,{done:!0,value:void 0}),i=()=>{if(e)return t();let r=this.read();return r===null?t():{done:!1,value:r}};return this.once("end",t),this.once(Qi,t),this.once(y,t),{next:i,throw:t,return:t,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(e){if(this[y])return e?this.emit("error",e):this.emit(y),this;this[y]=!0,this[x]=!0,this[O].length=0,this[v]=0;let t=this;return typeof t.close=="function"&&!this[Ut]&&t.close(),e?this.emit("error",e):this.emit(y),this}static get isStream(){return F.isStream}};F.Minipass=Zt});var Ke=d(W=>{"use strict";var Wr=W&&W.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(W,"__esModule",{value:!0});W.WriteStreamSync=W.WriteStream=W.ReadStreamSync=W.ReadStream=void 0;var ko=Wr(require("events")),z=Wr(require("fs")),xo=We(),jo=z.default.writev,Ee=Symbol("_autoClose"),$=Symbol("_close"),wt=Symbol("_ended"),p=Symbol("_fd"),rs=Symbol("_finished"),de=Symbol("_flags"),ns=Symbol("_flush"),ls=Symbol("_handleChunk"),cs=Symbol("_makeBuf"),Et=Symbol("_mode"),Yt=Symbol("_needDrain"),Ze=Symbol("_onerror"),Ye=Symbol("_onopen"),os=Symbol("_onread"),He=Symbol("_onwrite"),be=Symbol("_open"),V=Symbol("_path"),ye=Symbol("_pos"),ee=Symbol("_queue"),Ge=Symbol("_read"),as=Symbol("_readSize"),fe=Symbol("_reading"),yt=Symbol("_remain"),hs=Symbol("_size"),Kt=Symbol("_write"),Me=Symbol("_writing"),Vt=Symbol("_defaultFlag"),Le=Symbol("_errored"),$t=class extends xo.Minipass{[Le]=!1;[p];[V];[as];[fe]=!1;[hs];[yt];[Ee];constructor(e,t){if(t=t||{},super(t),this.readable=!0,this.writable=!1,typeof e!="string")throw new TypeError("path must be a string");this[Le]=!1,this[p]=typeof t.fd=="number"?t.fd:void 0,this[V]=e,this[as]=t.readSize||16*1024*1024,this[fe]=!1,this[hs]=typeof t.size=="number"?t.size:1/0,this[yt]=this[hs],this[Ee]=typeof t.autoClose=="boolean"?t.autoClose:!0,typeof this[p]=="number"?this[Ge]():this[be]()}get fd(){return this[p]}get path(){return this[V]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[be](){z.default.open(this[V],"r",(e,t)=>this[Ye](e,t))}[Ye](e,t){e?this[Ze](e):(this[p]=t,this.emit("open",t),this[Ge]())}[cs](){return Buffer.allocUnsafe(Math.min(this[as],this[yt]))}[Ge](){if(!this[fe]){this[fe]=!0;let e=this[cs]();if(e.length===0)return process.nextTick(()=>this[os](null,0,e));z.default.read(this[p],e,0,e.length,null,(t,i,r)=>this[os](t,i,r))}}[os](e,t,i){this[fe]=!1,e?this[Ze](e):this[ls](t,i)&&this[Ge]()}[$](){if(this[Ee]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,z.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}[Ze](e){this[fe]=!0,this[$](),this.emit("error",e)}[ls](e,t){let i=!1;return this[yt]-=e,e>0&&(i=super.write(ethis[Ye](e,t))}[Ye](e,t){this[Vt]&&this[de]==="r+"&&e&&e.code==="ENOENT"?(this[de]="w",this[be]()):e?this[Ze](e):(this[p]=t,this.emit("open",t),this[Me]||this[ns]())}end(e,t){return e&&this.write(e,t),this[wt]=!0,!this[Me]&&!this[ee].length&&typeof this[p]=="number"&&this[He](null,0),this}write(e,t){return typeof e=="string"&&(e=Buffer.from(e,t)),this[wt]?(this.emit("error",new Error("write() after end()")),!1):this[p]===void 0||this[Me]||this[ee].length?(this[ee].push(e),this[Yt]=!0,!1):(this[Me]=!0,this[Kt](e),!0)}[Kt](e){z.default.write(this[p],e,0,e.length,this[ye],(t,i)=>this[He](t,i))}[He](e,t){e?this[Ze](e):(this[ye]!==void 0&&typeof t=="number"&&(this[ye]+=t),this[ee].length?this[ns]():(this[Me]=!1,this[wt]&&!this[rs]?(this[rs]=!0,this[$](),this.emit("finish")):this[Yt]&&(this[Yt]=!1,this.emit("drain"))))}[ns](){if(this[ee].length===0)this[wt]&&this[He](null,0);else if(this[ee].length===1)this[Kt](this[ee].pop());else{let e=this[ee];this[ee]=[],jo(this[p],e,this[ye],(t,i)=>this[He](t,i))}}[$](){if(this[Ee]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,z.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}};W.WriteStream=Xt;var fs=class extends Xt{[be](){let e;if(this[Vt]&&this[de]==="r+")try{e=z.default.openSync(this[V],this[de],this[Et])}catch(t){if(t?.code==="ENOENT")return this[de]="w",this[be]();throw t}else e=z.default.openSync(this[V],this[de],this[Et]);this[Ye](null,e)}[$](){if(this[Ee]&&typeof this[p]=="number"){let e=this[p];this[p]=void 0,z.default.closeSync(e),this.emit("close")}}[Kt](e){let t=!0;try{this[He](null,z.default.writeSync(this[p],e,0,e.length,this[ye])),t=!1}finally{if(t)try{this[$]()}catch{}}}};W.WriteStreamSync=fs});var Qt=d(b=>{"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.dealias=b.isNoFile=b.isFile=b.isAsync=b.isSync=b.isAsyncNoFile=b.isSyncNoFile=b.isAsyncFile=b.isSyncFile=void 0;var Uo=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),qo=s=>!!s.sync&&!!s.file;b.isSyncFile=qo;var Wo=s=>!s.sync&&!!s.file;b.isAsyncFile=Wo;var Ho=s=>!!s.sync&&!s.file;b.isSyncNoFile=Ho;var Go=s=>!s.sync&&!s.file;b.isAsyncNoFile=Go;var Zo=s=>!!s.sync;b.isSync=Zo;var Yo=s=>!s.sync;b.isAsync=Yo;var Ko=s=>!!s.file;b.isFile=Ko;var Vo=s=>!s.file;b.isNoFile=Vo;var $o=s=>{let e=Uo.get(s);return e||s},Xo=(s={})=>{if(!s)return{};let e={};for(let[t,i]of Object.entries(s)){let r=$o(t);e[r]=i}return e.chmod===void 0&&e.noChmod===!1&&(e.chmod=!0),delete e.noChmod,e};b.dealias=Xo});var Ve=d(Jt=>{"use strict";Object.defineProperty(Jt,"__esModule",{value:!0});Jt.makeCommand=void 0;var bt=Qt(),Qo=(s,e,t,i,r)=>Object.assign((n=[],o,a)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(a=o,o=void 0),o=o?Array.from(o):[];let h=(0,bt.dealias)(n);if(r?.(h,o),(0,bt.isSyncFile)(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return s(h,o)}else if((0,bt.isAsyncFile)(h)){let l=e(h,o);return a?l.then(()=>a(),a):l}else if((0,bt.isSyncNoFile)(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return t(h,o)}else if((0,bt.isAsyncNoFile)(h)){if(typeof a=="function")throw new TypeError("callback only supported with file option");return i(h,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:e,syncNoFile:t,asyncNoFile:i,validate:r});Jt.makeCommand=Qo});var ds=d($e=>{"use strict";var Jo=$e&&$e.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty($e,"__esModule",{value:!0});$e.constants=void 0;var ea=Jo(require("zlib")),ta=ea.default.constants||{ZLIB_VERNUM:4736};$e.constants=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},ta))});var Ps=d(f=>{"use strict";var ia=f&&f.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),sa=f&&f.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),ra=f&&f.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;rs,ms=Gr?.writable===!0||Gr?.set!==void 0?s=>{Ae.Buffer.concat=s?la:ha}:s=>{},Ie=Symbol("_superWrite"),Fe=class extends Error{code;errno;constructor(e,t){super("zlib: "+e.message,{cause:e}),this.code=e.code,this.errno=e.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+e.message,Error.captureStackTrace(this,t??this.constructor)}get name(){return"ZlibError"}};f.ZlibError=Fe;var ps=Symbol("flushFlag"),St=class extends oa.Minipass{#e=!1;#i=!1;#s;#n;#r;#t;#o;get sawError(){return this.#e}get handle(){return this.#t}get flushFlag(){return this.#s}constructor(e,t){if(!e||typeof e!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(e),this.#s=e.flush??0,this.#n=e.finishFlush??0,this.#r=e.fullFlushFlag??0,typeof Hr[t]!="function")throw new TypeError("Compression method not supported: "+t);try{this.#t=new Hr[t](e)}catch(i){throw new Fe(i,this.constructor)}this.#o=i=>{this.#e||(this.#e=!0,this.close(),this.emit("error",i))},this.#t?.on("error",i=>this.#o(new Fe(i))),this.once("end",()=>this.close)}close(){this.#t&&(this.#t.close(),this.#t=void 0,this.emit("close"))}reset(){if(!this.#e)return(0,_s.default)(this.#t,"zlib binding closed"),this.#t.reset?.()}flush(e){this.ended||(typeof e!="number"&&(e=this.#r),this.write(Object.assign(Ae.Buffer.alloc(0),{[ps]:e})))}end(e,t,i){return typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&(t?this.write(e,t):this.write(e)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Ie](e){return super.write(e)}write(e,t,i){if(typeof t=="function"&&(i=t,t="utf8"),typeof e=="string"&&(e=Ae.Buffer.from(e,t)),this.#e)return;(0,_s.default)(this.#t,"zlib binding closed");let r=this.#t._handle,n=r.close;r.close=()=>{};let o=this.#t.close;this.#t.close=()=>{},ms(!0);let a;try{let l=typeof e[ps]=="number"?e[ps]:this.#s;a=this.#t._processChunk(e,l),ms(!1)}catch(l){ms(!1),this.#o(new Fe(l,this.write))}finally{this.#t&&(this.#t._handle=r,r.close=n,this.#t.close=o,this.#t.removeAllListeners("error"))}this.#t&&this.#t.on("error",l=>this.#o(new Fe(l,this.write)));let h;if(a)if(Array.isArray(a)&&a.length>0){let l=a[0];h=this[Ie](Ae.Buffer.from(l));for(let c=1;c{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(e,t)}finally{this.handle.flush=i}this.handle&&(this.#e=e,this.#i=t)}}}};f.Zlib=ie;var ws=class extends ie{constructor(e){super(e,"Deflate")}};f.Deflate=ws;var ys=class extends ie{constructor(e){super(e,"Inflate")}};f.Inflate=ys;var Es=class extends ie{#e;constructor(e){super(e,"Gzip"),this.#e=e&&!!e.portable}[Ie](e){return this.#e?(this.#e=!1,e[9]=255,super[Ie](e)):super[Ie](e)}};f.Gzip=Es;var bs=class extends ie{constructor(e){super(e,"Gunzip")}};f.Gunzip=bs;var Ss=class extends ie{constructor(e){super(e,"DeflateRaw")}};f.DeflateRaw=Ss;var gs=class extends ie{constructor(e){super(e,"InflateRaw")}};f.InflateRaw=gs;var Rs=class extends ie{constructor(e){super(e,"Unzip")}};f.Unzip=Rs;var ei=class extends St{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||te.constants.BROTLI_OPERATION_FINISH,e.fullFlushFlag=te.constants.BROTLI_OPERATION_FLUSH,super(e,t)}},Os=class extends ei{constructor(e){super(e,"BrotliCompress")}};f.BrotliCompress=Os;var vs=class extends ei{constructor(e){super(e,"BrotliDecompress")}};f.BrotliDecompress=vs;var ti=class extends St{constructor(e,t){e=e||{},e.flush=e.flush||te.constants.ZSTD_e_continue,e.finishFlush=e.finishFlush||te.constants.ZSTD_e_end,e.fullFlushFlag=te.constants.ZSTD_e_flush,super(e,t)}},Ts=class extends ti{constructor(e){super(e,"ZstdCompress")}};f.ZstdCompress=Ts;var Ds=class extends ti{constructor(e){super(e,"ZstdDecompress")}};f.ZstdDecompress=Ds});var Kr=d(Xe=>{"use strict";Object.defineProperty(Xe,"__esModule",{value:!0});Xe.parse=Xe.encode=void 0;var ca=(s,e)=>{if(Number.isSafeInteger(s))s<0?fa(s,e):ua(s,e);else throw Error("cannot encode number outside of javascript safe integer range");return e};Xe.encode=ca;var ua=(s,e)=>{e[0]=128;for(var t=e.length;t>1;t--)e[t-1]=s&255,s=Math.floor(s/256)},fa=(s,e)=>{e[0]=255;var t=!1;s=s*-1;for(var i=e.length;i>1;i--){var r=s&255;s=Math.floor(s/256),t?e[i-1]=Zr(r):r===0?e[i-1]=0:(t=!0,e[i-1]=Yr(r))}},da=s=>{let e=s[0],t=e===128?pa(s.subarray(1,s.length)):e===255?ma(s):null;if(t===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(t))throw Error("parsed number outside of javascript safe integer range");return t};Xe.parse=da;var ma=s=>{for(var e=s.length,t=0,i=!1,r=e-1;r>-1;r--){var n=Number(s[r]),o;i?o=Zr(n):n===0?o=n:(i=!0,o=Yr(n)),o!==0&&(t-=o*Math.pow(256,e-r-1))}return t},pa=s=>{for(var e=s.length,t=0,i=e-1;i>-1;i--){var r=Number(s[i]);r!==0&&(t+=r*Math.pow(256,e-i-1))}return t},Zr=s=>(255^s)&255,Yr=s=>(255^s)+1&255});var Ns=d(C=>{"use strict";Object.defineProperty(C,"__esModule",{value:!0});C.code=C.name=C.normalFsTypes=C.isName=C.isCode=void 0;var _a=s=>C.name.has(s);C.isCode=_a;var wa=s=>C.code.has(s);C.isName=wa;C.normalFsTypes=new Set(["0","","1","2","3","4","5","6","7","D"]);C.name=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]);C.code=new Map(Array.from(C.name).map(s=>[s[1],s[0]]))});var et=d(se=>{"use strict";var ya=se&&se.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Ea=se&&se.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Vr=se&&se.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;rs===void 0||s<0?void 0:s,As=class{cksumValid=!1;needPax=!1;nullBlock=!1;block;path;mode;uid;gid;size;cksum;#e="Unsupported";linkpath;uname;gname;devmaj=0;devmin=0;atime;ctime;mtime;charset;comment;constructor(e,t=0,i,r){Buffer.isBuffer(e)?this.decode(e,t||0,i,r):e&&this.#i(e)}decode(e,t,i,r){if(t||(t=0),!e||!(e.length>=t+512))throw new Error("need 512 bytes for header");let n=Ce(e,t+156,1),o=Je.normalFsTypes.has(n),a=o?i:void 0,h=o?r:void 0;if(this.path=a?.path??Ce(e,t,100),this.mode=a?.mode??h?.mode??Se(e,t+100,8),this.uid=a?.uid??h?.uid??Se(e,t+108,8),this.gid=a?.gid??h?.gid??Se(e,t+116,8),this.size=ba(a?.size??h?.size??Se(e,t+124,12)),this.mtime=a?.mtime??h?.mtime??Ms(e,t+136,12),this.cksum=Se(e,t+148,12),h&&this.#i(h,!0),a&&this.#i(a),Je.isCode(n)&&(this.#e=n||"0"),this.#e==="0"&&this.path.slice(-1)==="/"&&(this.#e="5"),this.#e==="5"&&(this.size=0),this.linkpath=Ce(e,t+157,100),e.subarray(t+257,t+265).toString()==="ustar\x0000")if(this.uname=a?.uname??h?.uname??Ce(e,t+265,32),this.gname=a?.gname??h?.gname??Ce(e,t+297,32),this.devmaj=a?.devmaj??h?.devmaj??Se(e,t+329,8)??0,this.devmin=a?.devmin??h?.devmin??Se(e,t+337,8)??0,e[t+475]!==0){let c=Ce(e,t+345,155);this.path=c+"/"+this.path}else{let c=Ce(e,t+345,130);c&&(this.path=c+"/"+this.path),this.atime=i?.atime??r?.atime??Ms(e,t+476,12),this.ctime=i?.ctime??r?.ctime??Ms(e,t+488,12)}let l=256;for(let c=t;c!(r==null||i==="size"&&Number(r)<0||i==="path"&&t||i==="linkpath"&&t||i==="global"))))}encode(e,t=0){if(e||(e=this.block=Buffer.alloc(512)),this.#e==="Unsupported"&&(this.#e="0"),!(e.length>=t+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=Sa(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=Be(e,t,100,n)||this.needPax,this.needPax=ge(e,t+100,8,this.mode)||this.needPax,this.needPax=ge(e,t+108,8,this.uid)||this.needPax,this.needPax=ge(e,t+116,8,this.gid)||this.needPax,this.needPax=ge(e,t+124,12,this.size)||this.needPax,this.needPax=Ls(e,t+136,12,this.mtime)||this.needPax,e[t+156]=Number(this.#e.codePointAt(0)),this.needPax=Be(e,t+157,100,this.linkpath)||this.needPax,e.write("ustar\x0000",t+257,8),this.needPax=Be(e,t+265,32,this.uname)||this.needPax,this.needPax=Be(e,t+297,32,this.gname)||this.needPax,this.needPax=ge(e,t+329,8,this.devmaj)||this.needPax,this.needPax=ge(e,t+337,8,this.devmin)||this.needPax,this.needPax=Be(e,t+345,i,o)||this.needPax,e[t+475]!==0?this.needPax=Be(e,t+345,155,o)||this.needPax:(this.needPax=Be(e,t+345,130,o)||this.needPax,this.needPax=Ls(e,t+476,12,this.atime)||this.needPax,this.needPax=Ls(e,t+488,12,this.ctime)||this.needPax);let a=256;for(let h=t;h{let i=s,r="",n,o=Qe.posix.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Qe.posix.dirname(i),i=Qe.posix.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=e?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=e?n=[i.slice(0,99),r,!0]:(i=Qe.posix.join(Qe.posix.basename(r),i),r=Qe.posix.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},Ce=(s,e,t)=>s.subarray(e,e+t).toString("utf8").replace(/\0.*/,""),Ms=(s,e,t)=>ga(Se(s,e,t)),ga=s=>s===void 0?void 0:new Date(s*1e3),Se=(s,e,t)=>Number(s[e])&128?$r.parse(s.subarray(e,e+t)):Oa(s,e,t),Ra=s=>isNaN(s)?void 0:s,Oa=(s,e,t)=>Ra(parseInt(s.subarray(e,e+t).toString("utf8").replace(/\0.*$/,"").trim(),8)),va={12:8589934591,8:2097151},ge=(s,e,t,i)=>i===void 0?!1:i>va[t]||i<0?($r.encode(i,s.subarray(e,e+t)),!0):(Ta(s,e,t,i),!1),Ta=(s,e,t,i)=>s.write(Da(i,t),e,t,"ascii"),Da=(s,e)=>Pa(Math.floor(s).toString(8),e),Pa=(s,e)=>(s.length===e-1?s:new Array(e-s.length-1).join("0")+s+" ")+"\0",Ls=(s,e,t,i)=>i===void 0?!1:ge(s,e,t,i.getTime()/1e3),Na=new Array(156).join("\0"),Be=(s,e,t,i)=>i===void 0?!1:(s.write(i+Na,e,t,"utf8"),i.length!==Buffer.byteLength(i)||i.length>t)});var si=d(ii=>{"use strict";Object.defineProperty(ii,"__esModule",{value:!0});ii.Pax=void 0;var Ma=require("node:path"),La=et(),Is=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(e,t=!1){this.atime=e.atime,this.charset=e.charset,this.comment=e.comment,this.ctime=e.ctime,this.dev=e.dev,this.gid=e.gid,this.global=t,this.gname=e.gname,this.ino=e.ino,this.linkpath=e.linkpath,this.mtime=e.mtime,this.nlink=e.nlink,this.path=e.path,this.size=e.size,this.uid=e.uid,this.uname=e.uname}encode(){let e=this.encodeBody();if(e==="")return Buffer.allocUnsafe(0);let t=Buffer.byteLength(e),i=512*Math.ceil(1+t/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new La.Header({path:("PaxHeader/"+(0,Ma.basename)(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:t,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(e,512,t,"utf8");for(let n=t+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(e,t,i=!1){return new s(Aa(Ia(e),t),i)}};ii.Pax=Is;var Aa=(s,e)=>e?Object.assign({},e,s):s,Ia=s=>s.replace(/\n$/,"").split(` +`).reduce(Fa,Object.create(null)),Fa=(s,e)=>{let t=parseInt(e,10);if(t!==Buffer.byteLength(e)+1)return s;e=e.slice((t+" ").length);let i=e.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=").replace(/\0.*/,"");switch(n){case"path":case"linkpath":case"type":case"charset":case"comment":case"gname":case"uname":s[n]=o;break;case"ctime":case"atime":case"mtime":s[n]=new Date(Number(o)*1e3);break;case"size":let a=+o;a>=0&&(s[n]=a);break;case"gid":case"uid":case"dev":case"ino":case"nlink":case"mode":s[n]=+o;break}return s}});var tt=d(ri=>{"use strict";Object.defineProperty(ri,"__esModule",{value:!0});ri.normalizeWindowsPath=void 0;var Ca=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;ri.normalizeWindowsPath=Ca!=="win32"?s=>String(s):s=>String(s).replaceAll(/\\/g,"/")});var Cs=d(oi=>{"use strict";Object.defineProperty(oi,"__esModule",{value:!0});oi.ReadEntry=void 0;var Ba=We(),ni=tt(),Fs=class extends Ba.Minipass{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(e,t,i){switch(super({}),this.pause(),this.extended=t,this.globalExtended=i,this.header=e,this.remain=e.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=e.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!e.path)throw new Error("no path provided for tar.ReadEntry");this.path=(0,ni.normalizeWindowsPath)(e.path),this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=this.remain,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=e.linkpath?(0,ni.normalizeWindowsPath)(e.linkpath):void 0,this.uname=e.uname,this.gname=e.gname,t&&this.#e(t),i&&this.#e(i,!0)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-t),this.blockRemain=Math.max(0,r-t),this.ignore?!0:i>=t?super.write(e):super.write(e.subarray(0,i))}#e(e,t=!1){e.path&&(e.path=(0,ni.normalizeWindowsPath)(e.path)),e.linkpath&&(e.linkpath=(0,ni.normalizeWindowsPath)(e.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(e).filter(([i,r])=>!(r==null||i==="path"&&t))))}};oi.ReadEntry=Fs});var hi=d(ai=>{"use strict";Object.defineProperty(ai,"__esModule",{value:!0});ai.warnMethod=void 0;var za=(s,e,t,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=t instanceof Error&&t.code||e,i.tarCode=e,!s.strict&&i.recoverable!==!1?(t instanceof Error&&(i=Object.assign(t,i),t=t.message),s.emit("warn",e,t,i)):t instanceof Error?s.emit("error",Object.assign(t,i)):s.emit("error",Object.assign(new Error(`${e}: ${t}`),i))};ai.warnMethod=za});var _i=d(pi=>{"use strict";Object.defineProperty(pi,"__esModule",{value:!0});pi.Parser=void 0;var ka=require("events"),Bs=Ps(),Xr=et(),Qr=si(),xa=Cs(),ja=hi(),Ua=1024*1024,qs=Buffer.from([31,139]),Ws=Buffer.from([40,181,47,253]),qa=Math.max(qs.length,Ws.length),H=Symbol("state"),ze=Symbol("writeEntry"),me=Symbol("readEntry"),zs=Symbol("nextEntry"),Jr=Symbol("processEntry"),re=Symbol("extendedHeader"),gt=Symbol("globalExtendedHeader"),Re=Symbol("meta"),en=Symbol("emitMeta"),_=Symbol("buffer"),pe=Symbol("queue"),Oe=Symbol("ended"),ks=Symbol("emittedEnd"),ke=Symbol("emit"),S=Symbol("unzip"),li=Symbol("consumeChunk"),ci=Symbol("consumeChunkSub"),xs=Symbol("consumeBody"),tn=Symbol("consumeMeta"),sn=Symbol("consumeHeader"),Rt=Symbol("consuming"),js=Symbol("bufferConcat"),ui=Symbol("maybeEnd"),it=Symbol("writing"),ne=Symbol("aborted"),fi=Symbol("onDone"),xe=Symbol("sawValidEntry"),di=Symbol("sawNullBlock"),mi=Symbol("sawEOF"),rn=Symbol("closeStream"),Wa=1e3,Ot=Symbol("compressedBytesRead"),Us=Symbol("decompressedBytesRead"),nn=Symbol("checkDecompressionRatio"),Ha=()=>!0,Hs=class extends ka.EventEmitter{file;strict;maxMetaEntrySize;filter;brotli;zstd;maxDecompressionRatio;writable=!0;readable=!1;[pe]=[];[_];[me];[ze];[H]="begin";[Re]="";[re];[gt];[Oe]=!1;[S];[ne]=!1;[xe];[di]=!1;[mi]=!1;[it]=!1;[Rt]=!1;[ks]=!1;[Ot]=0;[Us]=0;constructor(e={}){super(),this.file=e.file||"",this.on(fi,()=>{(this[H]==="begin"||this[xe]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),e.ondone?this.on(fi,e.ondone):this.on(fi,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!e.strict,this.maxDecompressionRatio=typeof e.maxDecompressionRatio=="number"?e.maxDecompressionRatio:Wa,this.maxMetaEntrySize=e.maxMetaEntrySize||Ua,this.filter=typeof e.filter=="function"?e.filter:Ha;let t=e.file&&(e.file.endsWith(".tar.br")||e.file.endsWith(".tbr"));this.brotli=!(e.gzip||e.zstd)&&e.brotli!==void 0?e.brotli:t?void 0:!1;let i=e.file&&(e.file.endsWith(".tar.zst")||e.file.endsWith(".tzst"));this.zstd=!(e.gzip||e.brotli)&&e.zstd!==void 0?e.zstd:i?!0:void 0,this.on("end",()=>this[rn]()),typeof e.onwarn=="function"&&this.on("warn",e.onwarn),typeof e.onReadEntry=="function"&&this.on("entry",e.onReadEntry)}warn(e,t,i={}){(0,ja.warnMethod)(this,e,t,i)}[sn](e,t){this[xe]===void 0&&(this[xe]=!1);let i;try{i=new Xr.Header(e,t,this[re],this[gt])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[di]?(this[mi]=!0,this[H]==="begin"&&(this[H]="header"),this[ke]("eof")):(this[di]=!0,this[ke]("nullBlock"));else if(this[di]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[ze]=new xa.ReadEntry(i,this[re],this[gt]);if(!this[xe])if(n.remain){let o=()=>{n.invalid||(this[xe]=!0)};n.on("end",o)}else this[xe]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[ke]("ignoredEntry",n),this[H]="ignore",n.resume()):n.size>0&&(this[Re]="",n.on("data",o=>this[Re]+=o),this[H]="meta"):(this[re]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[ke]("ignoredEntry",n),this[H]=n.remain?"ignore":"header",n.resume()):(n.remain?this[H]="body":(this[H]="header",n.end()),this[me]?this[pe].push(n):(this[pe].push(n),this[zs]())))}}}[rn](){queueMicrotask(()=>this.emit("close"))}[Jr](e){let t=!0;if(!e)this[me]=void 0,t=!1;else if(Array.isArray(e)){let[i,...r]=e;this.emit(i,...r)}else this[me]=e,this.emit("entry",e),e.emittedEnd||(e.on("end",()=>this[zs]()),t=!1);return t}[zs](){do;while(this[Jr](this[pe].shift()));if(this[pe].length===0){let e=this[me];!e||e.flowing||e.size===e.remain?this[it]||this.emit("drain"):e.once("drain",()=>this.emit("drain"))}}[xs](e,t){let i=this[ze];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=e.length&&t===0?e:e.subarray(t,t+r);return i.write(n),i.blockRemain||(this[H]="header",this[ze]=void 0,i.end()),n.length}[tn](e,t){let i=this[ze],r=this[xs](e,t);return!this[ze]&&i&&this[en](i),r}[ke](e,t,i){this[pe].length===0&&!this[me]?this.emit(e,t,i):this[pe].push([e,t,i])}[en](e){switch(this[ke]("meta",this[Re]),e.type){case"ExtendedHeader":case"OldExtendedHeader":this[re]=Qr.Pax.parse(this[Re],this[re],!1);break;case"GlobalExtendedHeader":this[gt]=Qr.Pax.parse(this[Re],this[gt],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let t=this[re]??Object.create(null);this[re]=t,t.path=this[Re].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let t=this[re]||Object.create(null);this[re]=t,t.linkpath=this[Re].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+e.type)}}abort(e){this[ne]||(this[ne]=!0,this.emit("abort",e),this.warn("TAR_ABORT",e,{recoverable:!1}))}[nn](e){this[Us]+=e.length;let t=this[Us]/this[Ot];return t>this.maxDecompressionRatio?(this.abort(new Error(`max decompression ratio exceeded: ${t.toFixed(2)} > ${this.maxDecompressionRatio}`)),!1):!0}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this[ne])return i?.(),!1;if((this[S]===void 0||this.brotli===void 0&&this[S]===!1)&&e){if(this[_]&&(e=Buffer.concat([this[_],e]),this[_]=void 0),e.length{this[nn](c)&&this[li](c)}),this[S].on("error",c=>{this[ne]||this.abort(c)}),this[S].on("end",()=>{this[Oe]=!0,this[li]()}),this[it]=!0,this[Ot]+=e.length;let l=!!this[S][h?"end":"write"](e);return this[it]=!1,i?.(),l}}this[it]=!0,this[S]?(this[Ot]+=e.length,this[S].write(e)):this[li](e),this[it]=!1;let n=this[pe].length>0?!1:this[me]?this[me].flowing:!0;return!n&&this[pe].length===0&&this[me]?.once("drain",()=>this.emit("drain")),i?.(),n}[js](e){e&&!this[ne]&&(this[_]=this[_]?Buffer.concat([this[_],e]):e)}[ui](){if(this[Oe]&&!this[ks]&&!this[ne]&&!this[Rt]){this[ks]=!0;let e=this[ze];if(e?.blockRemain){let t=this[_]?this[_].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${e.blockRemain} more bytes, only ${t} available)`,{entry:e}),this[_]&&e.write(this[_]),e.end()}this[ke](fi)}}[li](e){if(this[Rt]&&e)this[js](e);else if(!e&&!this[_])this[ui]();else if(e){if(this[Rt]=!0,this[_]){this[js](e);let t=this[_];this[_]=void 0,this[ci](t)}else this[ci](e);for(;this[_]&&this[_]?.length>=512&&!this[ne]&&!this[mi];){let t=this[_];this[_]=void 0,this[ci](t)}this[Rt]=!1}(!this[_]||this[Oe])&&this[ui]()}[ci](e){let t=0,i=e.length;for(;t+512<=i&&!this[ne]&&!this[mi];)switch(this[H]){case"begin":case"header":this[sn](e,t),t+=512;break;case"ignore":case"body":t+=this[xs](e,t);break;case"meta":t+=this[tn](e,t);break;default:throw new Error("invalid state: "+this[H])}t{"use strict";Object.defineProperty(wi,"__esModule",{value:!0});wi.stripTrailingSlashes=void 0;var Ga=s=>{let e=s.length-1,t=-1;for(;e>-1&&s.charAt(e)==="/";)t=e,e--;return t===-1?s:s.slice(0,t)};wi.stripTrailingSlashes=Ga});var rt=d(B=>{"use strict";var Za=B&&B.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Ya=B&&B.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Ka=B&&B.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=s.onReadEntry;s.onReadEntry=e?t=>{e(t),t.resume()}:t=>t.resume()},Ja=(s,e)=>{let t=new Map(e.map(n=>[(0,Gs.stripTrailingSlashes)(n),!0])),i=s.filter,r=(n,o="")=>{let a=o||(0,on.parse)(n).root||".",h;if(n===a)h=!1;else{let l=t.get(n);h=l!==void 0?l:r((0,on.dirname)(n),a)}return t.set(n,h),h};s.filter=i?(n,o)=>i(n,o)&&r((0,Gs.stripTrailingSlashes)(n)):n=>r((0,Gs.stripTrailingSlashes)(n))};B.filesFilter=Ja;var eh=s=>{let e=new Ei.Parser(s),t=s.file,i;try{i=st.default.openSync(t,"r");let r=st.default.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let t=new Ei.Parser(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{t.on("error",a),t.on("end",o),st.default.stat(r,(h,l)=>{if(h)a(h);else{let c=new $a.ReadStream(r,{readSize:i,size:l.size});c.on("error",a),c.pipe(t)}})})};B.list=(0,Xa.makeCommand)(eh,th,s=>new Ei.Parser(s),s=>new Ei.Parser(s),(s,e)=>{e?.length&&(0,B.filesFilter)(s,e),s.noResume||Qa(s)})});var an=d(bi=>{"use strict";Object.defineProperty(bi,"__esModule",{value:!0});bi.modeFix=void 0;var ih=(s,e,t)=>(s&=4095,t&&(s=(s|384)&-19),e&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);bi.modeFix=ih});var Zs=d(Si=>{"use strict";Object.defineProperty(Si,"__esModule",{value:!0});Si.stripAbsolutePath=void 0;var sh=require("node:path"),{isAbsolute:rh,parse:hn}=sh.win32,nh=s=>{let e="",t=hn(s);for(;rh(s)||t.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":t.root;s=s.slice(i.length),e+=i,t=hn(s)}return[e,s]};Si.stripAbsolutePath=nh});var Ks=d(nt=>{"use strict";Object.defineProperty(nt,"__esModule",{value:!0});nt.decode=nt.encode=void 0;var gi=["|","<",">","?",":"],Ys=gi.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),oh=new Map(gi.map((s,e)=>[s,Ys[e]])),ah=new Map(Ys.map((s,e)=>[s,gi[e]])),hh=s=>gi.reduce((e,t)=>e.split(t).join(oh.get(t)),s);nt.encode=hh;var lh=s=>Ys.reduce((e,t)=>e.split(t).join(ah.get(t)),s);nt.decode=lh});var nr=d(M=>{"use strict";var ch=M&&M.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),uh=M&&M.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),fh=M&&M.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;re?(s=(0,oe.normalizeWindowsPath)(s).replace(/^\.(\/|$)/,""),(0,dh.stripTrailingSlashes)(e)+"/"+s):(0,oe.normalizeWindowsPath)(s),ph=16*1024*1024,cn=Symbol("process"),un=Symbol("file"),fn=Symbol("directory"),$s=Symbol("symlink"),dn=Symbol("hardlink"),vt=Symbol("header"),Ri=Symbol("read"),Xs=Symbol("lstat"),Oi=Symbol("onlstat"),Qs=Symbol("onread"),Js=Symbol("onreadlink"),er=Symbol("openfile"),tr=Symbol("onopenfile"),ve=Symbol("close"),vi=Symbol("mode"),ir=Symbol("awaitDrain"),Vs=Symbol("ondrain"),he=Symbol("prefix"),Ti=class extends pn.Minipass{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#e=!1;constructor(e,t={}){let i=(0,yn.dealias)(t);super(),this.path=(0,oe.normalizeWindowsPath)(e),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||ph,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=(0,oe.normalizeWindowsPath)(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?(0,oe.normalizeWindowsPath)(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,a]=(0,bn.stripAbsolutePath)(this.path);o&&typeof a=="string"&&(this.path=a,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=mh.decode(this.path.replaceAll(/\\/g,"/")),e=e.replaceAll(/\\/g,"/")),this.absolute=(0,oe.normalizeWindowsPath)(i.absolute||ln.default.resolve(this.cwd,e)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[Oi](n):this[Xs]()}warn(e,t,i={}){return(0,Sn.warnMethod)(this,e,t,i)}emit(e,...t){return e==="error"&&(this.#e=!0),super.emit(e,...t)}[Xs](){ae.default.lstat(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Oi](t)})}[Oi](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=_h(e),this.emit("stat",e),this[cn]()}[cn](){switch(this.type){case"File":return this[un]();case"Directory":return this[fn]();case"SymbolicLink":return this[$s]();default:return this.end()}}[vi](e){return(0,wn.modeFix)(e,this.type==="Directory",this.portable)}[he](e){return gn(e,this.prefix)}[vt](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new _n.Header({path:this[he](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[he](this.linkpath):this.linkpath,mode:this[vi](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new En.Pax({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[he](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[he](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let e=this.header?.block;if(!e)throw new Error("failed to encode header");super.write(e)}[fn](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[vt](),this.end()}[$s](){ae.default.readlink(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[Js](t)})}[Js](e){this.linkpath=(0,oe.normalizeWindowsPath)(e),this[vt](),this.end()}[dn](e){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=(0,oe.normalizeWindowsPath)(ln.default.relative(this.cwd,e)),this.stat.size=0,this[vt](),this.end()}[un](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let e=`${this.stat.dev}:${this.stat.ino}`,t=this.linkCache.get(e);if(t?.indexOf(this.cwd)===0)return this[dn](t);this.linkCache.set(e,this.absolute)}if(this[vt](),this.stat.size===0)return this.end();this[er]()}[er](){ae.default.open(this.absolute,"r",(e,t)=>{if(e)return this.emit("error",e);this[tr](t)})}[tr](e){if(this.fd=e,this.#e)return this[ve]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let t=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(t),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[Ri]()}[Ri](){let{fd:e,buf:t,offset:i,length:r,pos:n}=this;if(e===void 0||t===void 0)throw new Error("cannot read file without first opening");ae.default.read(e,t,i,r,n,(o,a)=>{if(o)return this[ve](()=>this.emit("error",o));this[Qs](a)})}[ve](e=()=>{}){this.fd!==void 0&&ae.default.close(this.fd,e)}[Qs](e){if(e<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(e>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[ve](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(e===this.remain)for(let r=e;rthis[Vs]())}[ir](e){this.once("drain",e)}write(e,t,i){if(typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this.blockRemaine?this.emit("error",e):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[Ri]()}};M.WriteEntry=Ti;var sr=class extends Ti{sync=!0;[Xs](){this[Oi](ae.default.lstatSync(this.absolute))}[$s](){this[Js](ae.default.readlinkSync(this.absolute))}[er](){this[tr](ae.default.openSync(this.absolute,"r"))}[Ri](){let e=!0;try{let{fd:t,buf:i,offset:r,length:n,pos:o}=this;if(t===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let a=ae.default.readSync(t,i,r,n,o);this[Qs](a),e=!1}finally{if(e)try{this[ve](()=>{})}catch{}}}[ir](e){e()}[ve](e=()=>{}){this.fd!==void 0&&ae.default.closeSync(this.fd),e()}};M.WriteEntrySync=sr;var rr=class extends pn.Minipass{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(e,t,i={}){return(0,Sn.warnMethod)(this,e,t,i)}constructor(e,t={}){let i=(0,yn.dealias)(t);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=e;let{type:r}=e;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=(0,oe.normalizeWindowsPath)(e.path),this.mode=e.mode!==void 0?this[vi](e.mode):void 0,this.uid=this.portable?void 0:e.uid,this.gid=this.portable?void 0:e.gid,this.uname=this.portable?void 0:e.uname,this.gname=this.portable?void 0:e.gname,this.size=e.size,this.mtime=this.noMtime?void 0:i.mtime||e.mtime,this.atime=this.portable?void 0:e.atime,this.ctime=this.portable?void 0:e.ctime,this.linkpath=e.linkpath!==void 0?(0,oe.normalizeWindowsPath)(e.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[a,h]=(0,bn.stripAbsolutePath)(this.path);a&&typeof h=="string"&&(this.path=h,n=a)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.onWriteEntry?.(this),this.header=new _n.Header({path:this[he](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[he](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new En.Pax({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[he](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[he](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),e.pipe(this)}[he](e){return gn(e,this.prefix)}[vi](e){return(0,wn.modeFix)(e,this.type==="Directory",this.portable)}write(e,t,i){typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8"));let r=e.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(e,i)}end(e,t,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof e=="function"&&(i=e,t=void 0,e=void 0),typeof t=="function"&&(i=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,t??"utf8")),i&&this.once("finish",i),e?super.end(e,i):super.end(i),this}};M.WriteEntryTar=rr;var _h=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported"});var Rn=d(at=>{"use strict";Object.defineProperty(at,"__esModule",{value:!0});at.Node=at.Yallist=void 0;var or=class s{tail;head;length=0;static create(e=[]){return new s(e)}constructor(e=[]){for(let t of e)this.push(t)}*[Symbol.iterator](){for(let e=this.head;e;e=e.next)yield e.value}removeNode(e){if(e.list!==this)throw new Error("removing node which does not belong to this list");let t=e.next,i=e.prev;return t&&(t.prev=i),i&&(i.next=t),e===this.head&&(this.head=t),e===this.tail&&(this.tail=i),this.length--,e.next=void 0,e.prev=void 0,e.list=void 0,t}unshiftNode(e){if(e===this.head)return;e.list&&e.list.removeNode(e);let t=this.head;e.list=this,e.next=t,t&&(t.prev=e),this.head=e,this.tail||(this.tail=e),this.length++}pushNode(e){if(e===this.tail)return;e.list&&e.list.removeNode(e);let t=this.tail;e.list=this,e.prev=t,t&&(t.next=e),this.tail=e,this.head||(this.head=e),this.length++}push(...e){for(let t=0,i=e.length;t1)i=t;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=e(i,r.value,n),r=r.next;return i}reduceReverse(e,t){let i,r=this.tail;if(arguments.length>1)i=t;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=e(i,r.value,n),r=r.prev;return i}toArray(){let e=new Array(this.length);for(let t=0,i=this.head;i;t++)e[t]=i.value,i=i.next;return e}toArrayReverse(){let e=new Array(this.length);for(let t=0,i=this.tail;i;t++)e[t]=i.value,i=i.prev;return e}slice(e=0,t=this.length){t<0&&(t+=this.length),e<0&&(e+=this.length);let i=new s;if(tthis.length&&(t=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(t=this.length);let r=this.length,n=this.tail;for(;n&&r>t;r--)n=n.prev;for(;n&&r>e;r--,n=n.prev)i.push(n.value);return i}splice(e,t=0,...i){e>this.length&&(e=this.length-1),e<0&&(e=this.length+e);let r=this.head;for(let o=0;r&&o{"use strict";var bh=L&&L.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Sh=L&&L.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),gh=L&&L.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(e.gzip&&(typeof e.gzip!="object"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new ar.Gzip(e.gzip)),e.brotli&&(typeof e.brotli!="object"&&(e.brotli={}),this.zip=new ar.BrotliCompress(e.brotli)),e.zstd&&(typeof e.zstd!="object"&&(e.zstd={}),this.zip=new ar.ZstdCompress(e.zstd)),!this.zip)throw new Error("impossible");let t=this.zip;t.on("data",i=>super.write(i)),t.on("end",()=>super.end()),t.on("drain",()=>this[cr]()),this.on("resume",()=>t.resume())}else this.on("drain",this[cr]);this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,e.mtime&&(this.mtime=e.mtime),this.filter=typeof e.filter=="function"?e.filter:()=>!0,this[X]=new Oh.Yallist,this[Q]=0,this.jobs=Number(e.jobs)||4,this[Pt]=!1,this[Tt]=!1}[Nn](e){return super.write(e)}add(e){return this.write(e),this}end(e,t,i){return typeof e=="function"&&(i=e,e=void 0),typeof t=="function"&&(i=t,t=void 0),e&&this.add(e),this[Tt]=!0,this[je](),i&&i(),this}write(e){if(this[Tt])throw new Error("write after end");return typeof e=="string"?this[Ni](e):this[vn](e),this.flowing}[vn](e){let t=(0,ur.normalizeWindowsPath)(Dn.default.resolve(this.cwd,e.path));if(!this.filter(e.path,e))e.resume();else{let i=new Nt(e.path,t);i.entry=new fr.WriteEntryTar(e,this[lr](i)),i.entry.on("end",()=>this[hr](i)),this[Q]+=1,this[X].push(i)}this[je]()}[Ni](e){let t=(0,ur.normalizeWindowsPath)(Dn.default.resolve(this.cwd,e));this[X].push(new Nt(e,t)),this[je]()}[dr](e){e.pending=!0,this[Q]+=1;let t=this.follow?"stat":"lstat";Ii.default[t](e.absolute,(i,r)=>{e.pending=!1,this[Q]-=1,i?this.emit("error",i):this[Pi](e,r)})}[Pi](e,t){if(this.statCache.set(e.absolute,t),e.stat=t,!this.filter(e.path,t))e.ignore=!0;else if(t.isFile()&&t.nlink>1&&!this.linkCache.get(`${t.dev}:${t.ino}`)&&!this.sync)if(e===this[Te])this[Di](e);else{let i=`${t.dev}:${t.ino}`,r=this[Dt].get(i);r?r.push(e):this[Dt].set(i,[e]),e.pendingLink=!0,e.pending=!0}this[je]()}[mr](e){e.pending=!0,this[Q]+=1,Ii.default.readdir(e.absolute,(t,i)=>{if(e.pending=!1,this[Q]-=1,t)return this.emit("error",t);this[Mi](e,i)})}[Mi](e,t){this.readdirCache.set(e.absolute,t),e.readdir=t,this[je]()}[je](){if(!this[Pt]){this[Pt]=!0;for(let e=this[X].head;e&&this[Q]1){let i=`${t.dev}:${t.ino}`,r=this[Dt].get(i);if(r){this[Dt].delete(i);for(let n of r)n.pending=!1,this[Di](n)}}this[je]()}[Di](e){if(e.pending&&e.pendingLink&&e===this[Te]&&(e.pending=!1,e.pendingLink=!1),!e.pending){if(e.entry){e===this[Te]&&!e.piped&&this[Li](e);return}if(!e.stat){let t=this.statCache.get(e.absolute);t?this[Pi](e,t):this[dr](e)}if(e.stat&&!e.ignore){if(!this.noDirRecurse&&e.stat.isDirectory()&&!e.readdir){let t=this.readdirCache.get(e.absolute);if(t?this[Mi](e,t):this[mr](e),!e.readdir)return}if(e.entry=this[Tn](e),!e.entry){e.ignore=!0;return}e===this[Te]&&!e.piped&&this[Li](e)}}}[lr](e){return{onwarn:(t,i,r)=>this.warn(t,i,r),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[Tn](e){this[Q]+=1;try{return new this[Ai](e.path,this[lr](e)).on("end",()=>this[hr](e)).on("error",i=>this.emit("error",i))}catch(t){this.emit("error",t)}}[cr](){this[Te]&&this[Te].entry&&this[Te].entry.resume()}[Li](e){e.piped=!0,e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Ni](o+r)});let t=e.entry,i=this.zip;if(!t)throw new Error("cannot pipe without source");i?t.on("data",r=>{i.write(r)||t.pause()}):t.on("data",r=>{super.write(r)||t.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(e,t,i={}){(0,vh.warnMethod)(this,e,t,i)}};L.Pack=Fi;var pr=class extends Fi{sync=!0;constructor(e){super(e),this[Ai]=fr.WriteEntrySync}pause(){}resume(){}[dr](e){let t=this.follow?"statSync":"lstatSync";this[Pi](e,Ii.default[t](e.absolute))}[mr](e){this[Mi](e,Ii.default.readdirSync(e.absolute))}[Li](e){let t=e.entry,i=this.zip;if(e.readdir&&e.readdir.forEach(r=>{let n=e.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[Ni](o+r)}),!t)throw new Error("Cannot pipe without source");i?t.on("data",r=>{i.write(r)}):t.on("data",r=>{super[Nn](r)})}};L.PackSync=pr});var _r=d(ht=>{"use strict";var Th=ht&&ht.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ht,"__esModule",{value:!0});ht.create=void 0;var Mn=Ke(),Ln=Th(require("node:path")),An=rt(),Dh=Ve(),Bi=Ci(),Ph=(s,e)=>{let t=new Bi.PackSync(s),i=new Mn.WriteStreamSync(s.file,{mode:s.mode||438});t.pipe(i),In(t,e)},Nh=(s,e)=>{let t=new Bi.Pack(s),i=new Mn.WriteStream(s.file,{mode:s.mode||438});t.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),t.on("error",o)});return Fn(t,e).catch(n=>t.emit("error",n)),r},In=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,An.list)({file:Ln.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},Fn=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,An.list)({file:Ln.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(t);s.end()},Mh=(s,e)=>{let t=new Bi.PackSync(s);return In(t,e),t},Lh=(s,e)=>{let t=new Bi.Pack(s);return Fn(t,e).catch(i=>t.emit("error",i)),t};ht.create=(0,Dh.makeCommand)(Ph,Nh,Mh,Lh,(s,e)=>{if(!e?.length)throw new TypeError("no paths specified to add to archive")})});var Wn=d(lt=>{"use strict";var Ah=lt&<.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(lt,"__esModule",{value:!0});lt.getWriteFlag=void 0;var zn=Ah(require("fs")),Ih=process.env.__FAKE_PLATFORM__||process.platform,kn=Ih==="win32",{O_CREAT:xn,O_NOFOLLOW:Cn,O_TRUNC:jn,O_WRONLY:Un}=zn.default.constants,qn=Number(process.env.__FAKE_FS_O_FILENAME__)||zn.default.constants.UV_FS_O_FILEMAP||0,Fh=kn&&!!qn,Ch=512*1024,Bh=qn|jn|xn|Un,Bn=!kn&&typeof Cn=="number"?Cn|jn|xn|Un:null;lt.getWriteFlag=Bn!==null?()=>Bn:Fh?s=>s"w"});var Gn=d(le=>{"use strict";var Hn=le&&le.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(le,"__esModule",{value:!0});le.chownrSync=le.chownr=void 0;var ki=Hn(require("node:fs")),Mt=Hn(require("node:path")),wr=(s,e,t)=>{try{return ki.default.lchownSync(s,e,t)}catch(i){if(i?.code!=="ENOENT")throw i}},zi=(s,e,t,i)=>{ki.default.lchown(s,e,t,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},zh=(s,e,t,i,r)=>{if(e.isDirectory())(0,le.chownr)(Mt.default.resolve(s,e.name),t,i,n=>{if(n)return r(n);let o=Mt.default.resolve(s,e.name);zi(o,t,i,r)});else{let n=Mt.default.resolve(s,e.name);zi(n,t,i,r)}},kh=(s,e,t,i)=>{ki.default.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return zi(s,e,t,i);let o=n.length,a=null,h=l=>{if(!a){if(l)return i(a=l);if(--o===0)return zi(s,e,t,i)}};for(let l of n)zh(s,l,e,t,h)})};le.chownr=kh;var xh=(s,e,t,i)=>{e.isDirectory()&&(0,le.chownrSync)(Mt.default.resolve(s,e.name),t,i),wr(Mt.default.resolve(s,e.name),t,i)},jh=(s,e,t)=>{let i;try{i=ki.default.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return wr(s,e,t);throw n}for(let r of i)xh(s,r,e,t);return wr(s,e,t)};le.chownrSync=jh});var Zn=d(xi=>{"use strict";Object.defineProperty(xi,"__esModule",{value:!0});xi.CwdError=void 0;var yr=class extends Error{path;code;syscall="chdir";constructor(e,t){super(`${t}: Cannot cd into '${e}'`),this.path=e,this.code=t}get name(){return"CwdError"}};xi.CwdError=yr});var br=d(ji=>{"use strict";Object.defineProperty(ji,"__esModule",{value:!0});ji.SymlinkError=void 0;var Er=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(e,t){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=e,this.path=t}get name(){return"SymlinkError"}};ji.SymlinkError=Er});var Xn=d(De=>{"use strict";var gr=De&&De.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(De,"__esModule",{value:!0});De.mkdirSync=De.mkdir=void 0;var Yn=Gn(),j=gr(require("node:fs")),Uh=gr(require("node:fs/promises")),Ui=gr(require("node:path")),Kn=Zn(),_e=tt(),Vn=br(),qh=(s,e)=>{j.default.stat(s,(t,i)=>{(t||!i.isDirectory())&&(t=new Kn.CwdError(s,t?.code||"ENOTDIR")),e(t)})},Wh=(s,e,t)=>{s=(0,_e.normalizeWindowsPath)(s);let i=e.umask??18,r=e.mode|448,n=(r&i)!==0,o=e.uid,a=e.gid,h=typeof o=="number"&&typeof a=="number"&&(o!==e.processUid||a!==e.processGid),l=e.preserve,c=e.unlink,u=(0,_e.normalizeWindowsPath)(e.cwd),E=(w,P)=>{w?t(w):P&&h?(0,Yn.chownr)(P,o,a,xt=>E(xt)):n?j.default.chmod(s,r,t):t()};if(s===u)return qh(s,E);if(l)return Uh.default.mkdir(s,{mode:r,recursive:!0}).then(w=>E(null,w??void 0),E);let A=(0,_e.normalizeWindowsPath)(Ui.default.relative(u,s)).split("/");Sr(u,A,r,c,u,void 0,E)};De.mkdir=Wh;var Sr=(s,e,t,i,r,n,o)=>{if(e.length===0)return o(null,n);let a=e.shift(),h=(0,_e.normalizeWindowsPath)(Ui.default.resolve(s+"/"+a));j.default.mkdir(h,t,$n(h,e,t,i,r,n,o))},$n=(s,e,t,i,r,n,o)=>a=>{a?j.default.lstat(s,(h,l)=>{if(h)h.path=h.path&&(0,_e.normalizeWindowsPath)(h.path),o(h);else if(l.isDirectory())Sr(s,e,t,i,r,n,o);else if(i)j.default.unlink(s,c=>{if(c)return o(c);j.default.mkdir(s,t,$n(s,e,t,i,r,n,o))});else{if(l.isSymbolicLink())return o(new Vn.SymlinkError(s,s+"/"+e.join("/")));o(a)}}):(n=n||s,Sr(s,e,t,i,r,n,o))},Hh=s=>{let e=!1,t;try{e=j.default.statSync(s).isDirectory()}catch(i){t=i?.code}finally{if(!e)throw new Kn.CwdError(s,t??"ENOTDIR")}},Gh=(s,e)=>{s=(0,_e.normalizeWindowsPath)(s);let t=e.umask??18,i=e.mode|448,r=(i&t)!==0,n=e.uid,o=e.gid,a=typeof n=="number"&&typeof o=="number"&&(n!==e.processUid||o!==e.processGid),h=e.preserve,l=e.unlink,c=(0,_e.normalizeWindowsPath)(e.cwd),u=w=>{w&&a&&(0,Yn.chownrSync)(w,n,o),r&&j.default.chmodSync(s,i)};if(s===c)return Hh(c),u();if(h)return u(j.default.mkdirSync(s,{mode:i,recursive:!0})??void 0);let D=(0,_e.normalizeWindowsPath)(Ui.default.relative(c,s)).split("/"),A;for(let w=D.shift(),P=c;w&&(P+="/"+w);w=D.shift()){P=(0,_e.normalizeWindowsPath)(Ui.default.resolve(P));try{j.default.mkdirSync(P,i),A=A||P}catch{let xt=j.default.lstatSync(P);if(xt.isDirectory())continue;if(l){j.default.unlinkSync(P),j.default.mkdirSync(P,i),A=A||P;continue}else if(xt.isSymbolicLink())return new Vn.SymlinkError(P,P+"/"+D.join("/"))}}return u(A)};De.mkdirSync=Gh});var Jn=d(qi=>{"use strict";Object.defineProperty(qi,"__esModule",{value:!0});qi.normalizeUnicode=void 0;var Rr=Object.create(null),Qn=1e4,ct=new Set,Zh=s=>{ct.has(s)?ct.delete(s):Rr[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),ct.add(s);let e=Rr[s],t=ct.size-Qn;if(t>Qn/10){for(let i of ct)if(ct.delete(i),delete Rr[i],--t<=0)break}return e};qi.normalizeUnicode=Zh});var to=d(Wi=>{"use strict";Object.defineProperty(Wi,"__esModule",{value:!0});Wi.PathReservations=void 0;var eo=require("node:path"),Yh=Jn(),Kh=yi(),Vh=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,$h=Vh==="win32",Xh=s=>s.split("/").slice(0,-1).reduce((t,i)=>{let r=t.at(-1);return r!==void 0&&(i=(0,eo.join)(r,i)),t.push(i||"/"),t},[]),Or=class{#e=new Map;#i=new Map;#s=new Set;reserve(e,t){e=$h?["win32 parallelization disabled"]:e.map(r=>(0,Kh.stripTrailingSlashes)((0,eo.join)((0,Yh.normalizeUnicode)(r))));let i=new Set(e.map(r=>Xh(r)).reduce((r,n)=>r.concat(n)));this.#i.set(t,{dirs:i,paths:e});for(let r of e){let n=this.#e.get(r);n?n.push(t):this.#e.set(r,[t])}for(let r of i){let n=this.#e.get(r);if(!n)this.#e.set(r,[new Set([t])]);else{let o=n.at(-1);o instanceof Set?o.add(t):n.push(new Set([t]))}}return this.#r(t)}#n(e){let t=this.#i.get(e);if(!t)throw new Error("function does not have any path reservations");return{paths:t.paths.map(i=>this.#e.get(i)),dirs:[...t.dirs].map(i=>this.#e.get(i))}}check(e){let{paths:t,dirs:i}=this.#n(e);return t.every(r=>r&&r[0]===e)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(e))}#r(e){return this.#s.has(e)||!this.check(e)?!1:(this.#s.add(e),e(()=>this.#t(e)),!0)}#t(e){if(!this.#s.has(e))return!1;let t=this.#i.get(e);if(!t)throw new Error("invalid reservation");let{paths:i,dirs:r}=t,n=new Set;for(let o of i){let a=this.#e.get(o);if(!a||a?.[0]!==e)continue;let h=a[1];if(!h){this.#e.delete(o);continue}if(a.shift(),typeof h=="function")n.add(h);else for(let l of h)n.add(l)}for(let o of r){let a=this.#e.get(o),h=a?.[0];if(!(!a||!(h instanceof Set)))if(h.size===1&&a.length===1){this.#e.delete(o);continue}else if(h.size===1){a.shift();let l=a[0];typeof l=="function"&&n.add(l)}else h.delete(e)}return this.#s.delete(e),n.forEach(o=>this.#r(o)),!0}};Wi.PathReservations=Or});var io=d(Hi=>{"use strict";Object.defineProperty(Hi,"__esModule",{value:!0});Hi.umask=void 0;var Qh=()=>process.umask();Hi.umask=Qh});var Cr=d(k=>{"use strict";var Jh=k&&k.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),el=k&&k.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),fo=k&&k.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{if(!zt)return m.default.unlink(s,e);let t=s+".DELETE."+(0,mo.randomBytes)(16).toString("hex");m.default.rename(s,t,i=>{if(i)return e(i);m.default.unlink(t,e)})},cl=s=>{if(!zt)return m.default.unlinkSync(s);let e=s+".DELETE."+(0,mo.randomBytes)(16).toString("hex");m.default.renameSync(s,e),m.default.unlinkSync(e)},uo=(s,e,t)=>s!==void 0&&s===s>>>0?s:e!==void 0&&e===e>>>0?e:t,Yi=class extends sl.Parser{[Tr]=!1;[Bt]=!1;[Gi]=0;reservations=new nl.PathReservations;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(e={}){if(e.ondone=()=>{this[Tr]=!0,this[Dr]()},super(e),this.transform=e.transform,this.chmod=!!e.chmod,typeof e.uid=="number"||typeof e.gid=="number"){if(typeof e.uid!="number"||typeof e.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(e.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=e.preserveOwner===void 0&&typeof e.uid!="number"?process.getuid?.()===0:!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof e.maxDepth=="number"?e.maxDepth:hl,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||zt,this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=(0,U.normalizeWindowsPath)(g.default.resolve(e.cwd||process.cwd())),this.strip=Number(e.strip)||0,this.processUmask=this.chmod?typeof e.processUmask=="number"?e.processUmask:(0,ol.umask)():0,this.umask=typeof e.umask=="number"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on("entry",t=>this[ro](t))}warn(e,t,i={}){return(e==="TAR_BAD_ARCHIVE"||e==="TAR_ABORT")&&(i.recoverable=!1),super.warn(e,t,i)}[Dr](){this[Tr]&&this[Gi]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[vr](e,t){let i=e[t],{type:r}=e;if(!i||this.preservePaths)return!0;let[n,o]=(0,rl.stripAbsolutePath)(i),a=o.replaceAll(/\\/g,"/").split("/");if(a.includes("..")||zt&&/^[a-z]:\.\.$/i.test(a[0]??"")){if(t==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${t} contains '..'`,{entry:e,[t]:i}),!1;let h=g.default.posix.dirname(e.path),l=g.default.posix.normalize(g.default.posix.join(h,a.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${t} escapes extraction directory`,{entry:e,[t]:i}),!1}return n&&(e[t]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${t}`,{entry:e,[t]:i})),!0}[lo](e){let t=(0,U.normalizeWindowsPath)(e.path),i=t.split("/");if(this.strip){if(i.length=this.strip)e.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),e.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:e,path:t,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[vr](e,"path")||!this[vr](e,"linkpath"))return!1;if(e.absolute=g.default.isAbsolute(e.path)?(0,U.normalizeWindowsPath)(g.default.resolve(e.path)):(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,e.path)),!this.preservePaths&&typeof e.absolute=="string"&&e.absolute.indexOf(this.cwd+"/")!==0&&e.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:e,path:(0,U.normalizeWindowsPath)(e.path),resolvedPath:e.absolute,cwd:this.cwd}),!1;if(e.absolute===this.cwd&&e.type!=="Directory"&&e.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=g.default.win32.parse(String(e.absolute));e.absolute=r+so.encode(String(e.absolute).slice(r.length));let{root:n}=g.default.win32.parse(e.path);e.path=n+so.encode(e.path.slice(n.length))}return!0}[ro](e){if(!this[lo](e))return e.resume();switch(il.default.equal(typeof e.absolute,"string"),e.type){case"Directory":case"GNUDumpDir":e.mode&&(e.mode=e.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[Pr](e);default:return this[ho](e)}}[T](e,t){e.name==="CwdError"?this.emit("error",e):(this.warn("TAR_ENTRY_ERROR",e,{entry:t}),this[ut](),t.resume())}[Pe](e,t,i){(0,_o.mkdir)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t},i)}[It](e){return this.forceChown||this.preserveOwner&&(typeof e.uid=="number"&&e.uid!==this.processUid||typeof e.gid=="number"&&e.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[Ft](e){return uo(this.uid,e.uid,this.processUid)}[Ct](e){return uo(this.gid,e.gid,this.processGid)}[Mr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=new tl.WriteStream(String(e.absolute),{flags:(0,po.getWriteFlag)(e.size),mode:i,autoClose:!1});r.on("error",h=>{r.fd&&m.default.close(r.fd,()=>{}),r.write=()=>!0,this[T](h,e),t()});let n=1,o=h=>{if(h){r.fd&&m.default.close(r.fd,()=>{}),this[T](h,e),t();return}--n===0&&r.fd!==void 0&&m.default.close(r.fd,l=>{l?this[T](l,e):this[ut](),t()})};r.on("finish",()=>{let h=String(e.absolute),l=r.fd;if(typeof l=="number"&&e.mtime&&!this.noMtime){n++;let c=e.atime||new Date,u=e.mtime;m.default.futimes(l,c,u,E=>E?m.default.utimes(h,c,u,D=>o(D&&E)):o())}if(typeof l=="number"&&this[It](e)){n++;let c=this[Ft](e),u=this[Ct](e);typeof c=="number"&&typeof u=="number"&&m.default.fchown(l,c,u,E=>E?m.default.chown(h,c,u,D=>o(D&&E)):o())}o()});let a=this.transform&&this.transform(e)||e;a!==e&&(a.on("error",h=>{this[T](h,e),t()}),e.pipe(a)),a.pipe(r)}[Lr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode;this[Pe](String(e.absolute),i,r=>{if(r){this[T](r,e),t();return}let n=1,o=()=>{--n===0&&(t(),this[ut](),e.resume())};e.mtime&&!this.noMtime&&(n++,m.default.utimes(String(e.absolute),e.atime||new Date,e.mtime,o)),this[It](e)&&(n++,m.default.chown(String(e.absolute),Number(this[Ft](e)),Number(this[Ct](e)),o)),o()})}[ho](e){e.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[oo](e,t){let i=(0,U.normalizeWindowsPath)(g.default.relative(this.cwd,g.default.resolve(g.default.dirname(String(e.absolute)),String(e.linkpath)))).split("/");this[At](e,this.cwd,i,()=>this[Zi](e,String(e.linkpath),"symlink",t),r=>{this[T](r,e),t()})}[ao](e,t){let i=(0,U.normalizeWindowsPath)(g.default.resolve(this.cwd,String(e.linkpath))),r=(0,U.normalizeWindowsPath)(String(e.linkpath)).split("/");this[At](e,this.cwd,r,()=>this[Zi](e,i,"link",t),n=>{this[T](n,e),t()})}[At](e,t,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let a=g.default.resolve(t,o);m.default.lstat(a,(h,l)=>{if(h)return r();if(l?.isSymbolicLink())return n(new wo.SymlinkError(a,g.default.resolve(a,i.join("/"))));this[At](e,a,i,r,n)})}[co](){this[Gi]++}[ut](){this[Gi]--,this[Dr]()}[Ar](e){this[ut](),e.resume()}[Nr](e,t){return e.type==="File"&&!this.unlink&&t.isFile()&&t.nlink<=1&&!zt}[Pr](e){this[co]();let t=[e.path];e.linkpath&&t.push(e.linkpath),this.reservations.reserve(t,i=>this[no](e,i))}[no](e,t){let i=a=>{t(a)},r=()=>{this[Pe](this.cwd,this.dmode,a=>{if(a){this[T](a,e),i();return}this[Bt]=!0,n()})},n=()=>{if(e.absolute!==this.cwd){let a=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(a!==this.cwd)return this[Pe](a,this.dmode,h=>{if(h){this[T](h,e),i();return}o()})}o()},o=()=>{m.default.lstat(String(e.absolute),(a,h)=>{if(h&&(this.keep||this.newer&&h.mtime>(e.mtime??h.mtime))){this[Ar](e),i();return}if(a||this[Nr](e,h))return this[G](null,e,i);if(h.isDirectory()){if(e.type==="Directory"){let l=this.chmod&&e.mode&&(h.mode&4095)!==e.mode,c=u=>this[G](u??null,e,i);return l?m.default.chmod(String(e.absolute),Number(e.mode),c):c()}if(e.absolute!==this.cwd)return m.default.rmdir(String(e.absolute),l=>this[G](l??null,e,i))}if(e.absolute===this.cwd)return this[G](null,e,i);ll(String(e.absolute),l=>this[G](l??null,e,i))})};this[Bt]?n():r()}[G](e,t,i){if(e){this[T](e,t),i();return}switch(t.type){case"File":case"OldFile":case"ContiguousFile":return this[Mr](t,i);case"Link":return this[ao](t,i);case"SymbolicLink":return this[oo](t,i);case"Directory":case"GNUDumpDir":return this[Lr](t,i)}}[Zi](e,t,i,r){m.default[i](t,String(e.absolute),n=>{n?this[T](n,e):(this[ut](),e.resume()),r()})}};k.Unpack=Yi;var Lt=s=>{try{return[null,s()]}catch(e){return[e,null]}},Ir=class extends Yi{sync=!0;[G](e,t){return super[G](e,t,()=>{})}[Pr](e){if(!this[Bt]){let n=this[Pe](this.cwd,this.dmode);if(n)return this[T](n,e);this[Bt]=!0}if(e.absolute!==this.cwd){let n=(0,U.normalizeWindowsPath)(g.default.dirname(String(e.absolute)));if(n!==this.cwd){let o=this[Pe](n,this.dmode);if(o)return this[T](o,e)}}let[t,i]=Lt(()=>m.default.lstatSync(String(e.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(e.mtime??i.mtime)))return this[Ar](e);if(t||this[Nr](e,i))return this[G](null,e);if(i.isDirectory()){if(e.type==="Directory"){let o=this.chmod&&e.mode&&(i.mode&4095)!==e.mode,[a]=o?Lt(()=>{m.default.chmodSync(String(e.absolute),Number(e.mode))}):[];return this[G](a,e)}let[n]=Lt(()=>m.default.rmdirSync(String(e.absolute)));this[G](n,e)}let[r]=e.absolute===this.cwd?[]:Lt(()=>cl(String(e.absolute)));this[G](r,e)}[Mr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.fmode,r=a=>{let h;try{m.default.closeSync(n)}catch(l){h=l}(a||h)&&this[T](a||h,e),t()},n;try{n=m.default.openSync(String(e.absolute),(0,po.getWriteFlag)(e.size),i)}catch(a){return r(a)}let o=this.transform&&this.transform(e)||e;o!==e&&(o.on("error",a=>this[T](a,e)),e.pipe(o)),o.on("data",a=>{try{m.default.writeSync(n,a,0,a.length)}catch(h){r(h)}}),o.on("end",()=>{let a=null;if(e.mtime&&!this.noMtime){let h=e.atime||new Date,l=e.mtime;try{m.default.futimesSync(n,h,l)}catch(c){try{m.default.utimesSync(String(e.absolute),h,l)}catch{a=c}}}if(this[It](e)){let h=this[Ft](e),l=this[Ct](e);try{m.default.fchownSync(n,Number(h),Number(l))}catch(c){try{m.default.chownSync(String(e.absolute),Number(h),Number(l))}catch{a=a||c}}}r(a)})}[Lr](e,t){let i=typeof e.mode=="number"?e.mode&4095:this.dmode,r=this[Pe](String(e.absolute),i);if(r){this[T](r,e),t();return}if(e.mtime&&!this.noMtime)try{m.default.utimesSync(String(e.absolute),e.atime||new Date,e.mtime)}catch{}if(this[It](e))try{m.default.chownSync(String(e.absolute),Number(this[Ft](e)),Number(this[Ct](e)))}catch{}t(),e.resume()}[Pe](e,t){try{return(0,_o.mkdirSync)((0,U.normalizeWindowsPath)(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t})}catch(i){return i}}[At](e,t,i,r,n){if(this.preservePaths||i.length===0)return r();let o=t;for(let a of i){o=g.default.resolve(o,a);let[h,l]=Lt(()=>m.default.lstatSync(o));if(h)return r();if(l.isSymbolicLink())return n(new wo.SymlinkError(o,g.default.resolve(t,i.join("/"))))}r()}[Zi](e,t,i,r){let n=`${i}Sync`;try{m.default[n](t,String(e.absolute)),r(),e.resume()}catch(o){return this[T](o,e)}}};k.UnpackSync=Ir});var Br=d(Z=>{"use strict";var ul=Z&&Z.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),fl=Z&&Z.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),dl=Z&&Z.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r{let e=new Ki.UnpackSync(s),t=s.file,i=Eo.default.statSync(t),r=s.maxReadSize||16*1024*1024;new yo.ReadStreamSync(t,{readSize:r,size:i.size}).pipe(e)},yl=(s,e)=>{let t=new Ki.Unpack(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{t.on("error",a),t.on("close",o),Eo.default.stat(r,(h,l)=>{if(h)a(h);else{let c=new yo.ReadStream(r,{readSize:i,size:l.size});c.on("error",a),c.pipe(t)}})})};Z.extract=(0,_l.makeCommand)(wl,yl,s=>new Ki.UnpackSync(s),s=>new Ki.Unpack(s),(s,e)=>{e?.length&&(0,pl.filesFilter)(s,e)})});var Vi=d(ft=>{"use strict";var bo=ft&&ft.__importDefault||function(s){return s&&s.__esModule?s:{default:s}};Object.defineProperty(ft,"__esModule",{value:!0});ft.replace=void 0;var So=Ke(),q=bo(require("node:fs")),go=bo(require("node:path")),Ro=et(),Oo=rt(),El=Ve(),bl=Qt(),vo=Ci(),Sl=(s,e)=>{let t=new vo.PackSync(s),i=!0,r,n;try{try{r=q.default.openSync(s.file,"r+")}catch(h){if(h?.code==="ENOENT")r=q.default.openSync(s.file,"w+");else throw h}let o=q.default.fstatSync(r),a=Buffer.alloc(512);e:for(n=0;no.size)break;n+=l,s.mtimeCache&&h.mtime&&s.mtimeCache.set(String(h.path),h.mtime)}i=!1,gl(s,t,n,r,e)}finally{if(i)try{q.default.closeSync(r)}catch{}}},gl=(s,e,t,i,r)=>{let n=new So.WriteStreamSync(s.file,{fd:i,start:t});e.pipe(n),Ol(e,r)},Rl=(s,e)=>{e=Array.from(e);let t=new vo.Pack(s),i=(n,o,a)=>{let h=(D,A)=>{D?q.default.close(n,w=>a(D)):a(null,A)},l=0;if(o===0)return h(null,0);let c=0,u=Buffer.alloc(512),E=(D,A)=>{if(D||A===void 0)return h(D);if(c+=A,c<512&&A)return q.default.read(n,u,c,u.length-c,l+c,E);if(l===0&&u[0]===31&&u[1]===139)return h(new Error("cannot append to compressed archives"));if(c<512)return h(null,l);let w=new Ro.Header(u);if(!w.cksumValid)return h(null,l);let P=512*Math.ceil((w.size??0)/512);if(l+P+512>o||(l+=P+512,l>=o))return h(null,l);s.mtimeCache&&w.mtime&&s.mtimeCache.set(String(w.path),w.mtime),c=0,q.default.read(n,u,0,512,l,E)};q.default.read(n,u,0,512,l,E)};return new Promise((n,o)=>{t.on("error",o);let a="r+",h=(l,c)=>{if(l&&l.code==="ENOENT"&&a==="r+")return a="w+",q.default.open(s.file,a,h);if(l||!c)return o(l);q.default.fstat(c,(u,E)=>{if(u)return q.default.close(c,()=>o(u));i(c,E.size,(D,A)=>{if(D)return o(D);let w=new So.WriteStream(s.file,{fd:c,start:A});t.pipe(w),w.on("error",o),w.on("close",n),vl(t,e)})})};q.default.open(s.file,a,h)})},Ol=(s,e)=>{e.forEach(t=>{t.charAt(0)==="@"?(0,Oo.list)({file:go.default.resolve(s.cwd,t.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t)}),s.end()},vl=async(s,e)=>{for(let t of e)t.charAt(0)==="@"?await(0,Oo.list)({file:go.default.resolve(String(s.cwd),t.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(t);s.end()};ft.replace=(0,El.makeCommand)(Sl,Rl,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,e)=>{if(!(0,bl.isFile)(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!e?.length)throw new TypeError("no paths specified to add/replace")})});var zr=d($i=>{"use strict";Object.defineProperty($i,"__esModule",{value:!0});$i.update=void 0;var Tl=Ve(),kt=Vi();$i.update=(0,Tl.makeCommand)(kt.replace.syncFile,kt.replace.asyncFile,kt.replace.syncNoFile,kt.replace.asyncNoFile,(s,e=[])=>{kt.replace.validate?.(s,e),Dl(s)});var Dl=s=>{let e=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=e?(t,i)=>e(t,i)&&!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0)):(t,i)=>!((s.mtimeCache?.get(t)??i.mtime??0)>(i.mtime??0))}});var To=exports&&exports.__createBinding||(Object.create?(function(s,e,t,i){i===void 0&&(i=t);var r=Object.getOwnPropertyDescriptor(e,t);(!r||("get"in r?!e.__esModule:r.writable||r.configurable))&&(r={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,i,r)}):(function(s,e,t,i){i===void 0&&(i=t),s[i]=e[t]})),Pl=exports&&exports.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),Y=exports&&exports.__exportStar||function(s,e){for(var t in s)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&To(e,s,t)},Nl=exports&&exports.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var i=[];for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(i[i.length]=r);return i},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var i=s(e),r=0;r p - : (p) => p && p.replaceAll(/\\/g, '/'); + (p) => String(p) + : (p) => String(p).replaceAll(/\\/g, '/'); //# sourceMappingURL=normalize-windows-path.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/tar/dist/commonjs/parse.js b/deps/npm/node_modules/tar/dist/commonjs/parse.js index 999873ec64032e..2968e861ee68e5 100644 --- a/deps/npm/node_modules/tar/dist/commonjs/parse.js +++ b/deps/npm/node_modules/tar/dist/commonjs/parse.js @@ -60,6 +60,10 @@ const SAW_VALID_ENTRY = Symbol('sawValidEntry'); const SAW_NULL_BLOCK = Symbol('sawNullBlock'); const SAW_EOF = Symbol('sawEOF'); const CLOSESTREAM = Symbol('closeStream'); +const MAX_DECOMPRESSION_RATIO = 1000; +const COMPRESSEDBYTESREAD = Symbol('compressedBytesRead'); +const DECOMPRESSEDBYTESREAD = Symbol('decompressedBytesRead'); +const CHECKDECOMPRESSIONRATIO = Symbol('checkDecompressionRatio'); const noop = () => true; class Parser extends events_1.EventEmitter { file; @@ -68,6 +72,7 @@ class Parser extends events_1.EventEmitter { filter; brotli; zstd; + maxDecompressionRatio; writable = true; readable = false; [QUEUE] = []; @@ -87,6 +92,8 @@ class Parser extends events_1.EventEmitter { [WRITING] = false; [CONSUMING] = false; [EMITTEDEND] = false; + [COMPRESSEDBYTESREAD] = 0; + [DECOMPRESSEDBYTESREAD] = 0; constructor(opt = {}) { super(); this.file = opt.file || ''; @@ -109,6 +116,10 @@ class Parser extends events_1.EventEmitter { }); } this.strict = !!opt.strict; + this.maxDecompressionRatio = + typeof opt.maxDecompressionRatio === 'number' ? + opt.maxDecompressionRatio + : MAX_DECOMPRESSION_RATIO; this.maxMetaEntrySize = opt.maxMetaEntrySize || maxMetaEntrySize; this.filter = typeof opt.filter === 'function' ? opt.filter : noop; // Unlike gzip, brotli doesn't have any magic bytes to identify it @@ -362,11 +373,23 @@ class Parser extends events_1.EventEmitter { } } abort(error) { + if (this[ABORTED]) { + return; + } this[ABORTED] = true; this.emit('abort', error); // always throws, even in non-strict mode this.warn('TAR_ABORT', error, { recoverable: false }); } + [CHECKDECOMPRESSIONRATIO](chunk) { + this[DECOMPRESSEDBYTESREAD] += chunk.length; + const ratio = this[DECOMPRESSEDBYTESREAD] / this[COMPRESSEDBYTESREAD]; + if (ratio > this.maxDecompressionRatio) { + this.abort(new Error(`max decompression ratio exceeded: ${ratio.toFixed(2)} > ${this.maxDecompressionRatio}`)); + return false; + } + return true; + } write(chunk, encoding, cb) { if (typeof encoding === 'function') { cb = encoding; @@ -450,13 +473,22 @@ class Parser extends events_1.EventEmitter { this[UNZIP] === undefined ? new minizlib_1.Unzip({}) : isZstd ? new minizlib_1.ZstdDecompress({}) : new minizlib_1.BrotliDecompress({}); - this[UNZIP].on('data', chunk => this[CONSUMECHUNK](chunk)); - this[UNZIP].on('error', er => this.abort(er)); + this[UNZIP].on('data', chunk => { + if (this[CHECKDECOMPRESSIONRATIO](chunk)) { + this[CONSUMECHUNK](chunk); + } + }); + this[UNZIP].on('error', er => { + if (!this[ABORTED]) { + this.abort(er); + } + }); this[UNZIP].on('end', () => { this[ENDED] = true; this[CONSUMECHUNK](); }); this[WRITING] = true; + this[COMPRESSEDBYTESREAD] += chunk.length; const ret = !!this[UNZIP][ended ? 'end' : 'write'](chunk); this[WRITING] = false; cb?.(); @@ -465,6 +497,7 @@ class Parser extends events_1.EventEmitter { } this[WRITING] = true; if (this[UNZIP]) { + this[COMPRESSEDBYTESREAD] += chunk.length; this[UNZIP].write(chunk); } else { @@ -495,7 +528,7 @@ class Parser extends events_1.EventEmitter { !this[CONSUMING]) { this[EMITTEDEND] = true; const entry = this[WRITEENTRY]; - if (entry && entry.blockRemain) { + if (entry?.blockRemain) { // truncated, likely a damaged file const have = this[BUFFER] ? this[BUFFER].length : 0; this.warn('TAR_BAD_ARCHIVE', `Truncated input (needed ${entry.blockRemain} more bytes, only ${have} available)`, { entry }); @@ -589,8 +622,10 @@ class Parser extends events_1.EventEmitter { if (!this[ABORTED]) { if (this[UNZIP]) { /* c8 ignore start */ - if (chunk) + if (chunk) { + this[COMPRESSEDBYTESREAD] += chunk.length; this[UNZIP].write(chunk); + } /* c8 ignore stop */ this[UNZIP].end(); } diff --git a/deps/npm/node_modules/tar/dist/commonjs/pax.js b/deps/npm/node_modules/tar/dist/commonjs/pax.js index d30c0f3efbe9ea..fce37f63058e10 100644 --- a/deps/npm/node_modules/tar/dist/commonjs/pax.js +++ b/deps/npm/node_modules/tar/dist/commonjs/pax.js @@ -147,12 +147,36 @@ const parseKVLine = (set, line) => { return set; } const k = r.replace(/^SCHILY\.(dev|ino|nlink)/, '$1'); - const v = kv.join('='); - set[k] = - /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) ? - new Date(Number(v) * 1000) - : /^[0-9]+$/.test(v) ? +v - : v; + const v = kv.join('=').replace(/\0.*/, ''); + switch (k) { + case 'path': + case 'linkpath': + case 'type': + case 'charset': + case 'comment': + case 'gname': + case 'uname': + set[k] = v; + break; + case 'ctime': + case 'atime': + case 'mtime': + set[k] = new Date(Number(v) * 1000); + break; + case 'size': + const s = +v; + if (s >= 0) + set[k] = s; + break; + case 'gid': + case 'uid': + case 'dev': + case 'ino': + case 'nlink': + case 'mode': + set[k] = +v; + break; + } return set; }; //# sourceMappingURL=pax.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/tar/dist/commonjs/unpack.js b/deps/npm/node_modules/tar/dist/commonjs/unpack.js index 3df1031d84cbe5..0e9f861abb87cb 100644 --- a/deps/npm/node_modules/tar/dist/commonjs/unpack.js +++ b/deps/npm/node_modules/tar/dist/commonjs/unpack.js @@ -185,7 +185,7 @@ class Unpack extends parse_js_1.Parser { // default true for root this.preserveOwner = opt.preserveOwner === undefined && typeof opt.uid !== 'number' ? - !!(process.getuid && process.getuid() === 0) + !!(process.getuid?.() === 0) : !!opt.preserveOwner; this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ? @@ -406,7 +406,7 @@ class Unpack extends parse_js_1.Parser { } } [MKDIR](dir, mode, cb) { - (0, mkdir_js_1.mkdir)((0, normalize_windows_path_js_1.normalizeWindowsPath)(dir), { + void (0, mkdir_js_1.mkdir)((0, normalize_windows_path_js_1.normalizeWindowsPath)(dir), { uid: this.uid, gid: this.gid, processUid: this.processUid, diff --git a/deps/npm/node_modules/tar/dist/esm/header.js b/deps/npm/node_modules/tar/dist/esm/header.js index fe8c09928e46c9..068c4ea039885e 100644 --- a/deps/npm/node_modules/tar/dist/esm/header.js +++ b/deps/npm/node_modules/tar/dist/esm/header.js @@ -5,6 +5,7 @@ import { posix as pathModule } from 'node:path'; import * as large from './large-numbers.js'; import * as types from './types.js'; +const notNegative = (n) => n === undefined || n < 0 ? undefined : n; export class Header { cksumValid = false; needPax = false; @@ -63,10 +64,9 @@ export class Header { exForFields?.uid ?? gexForFields?.uid ?? decNumber(buf, off + 108, 8); this.gid = exForFields?.gid ?? gexForFields?.gid ?? decNumber(buf, off + 116, 8); - this.size = - exForFields?.size ?? - gexForFields?.size ?? - decNumber(buf, off + 124, 12); + this.size = notNegative(exForFields?.size ?? + gexForFields?.size ?? + decNumber(buf, off + 124, 12)); this.mtime = exForFields?.mtime ?? gexForFields?.mtime ?? @@ -152,6 +152,7 @@ export class Header { // null/undefined values are ignored. return !(v === null || v === undefined || + (k === 'size' && Number(v) < 0) || (k === 'path' && gex) || (k === 'linkpath' && gex) || k === 'global'); diff --git a/deps/npm/node_modules/tar/dist/esm/index.min.js b/deps/npm/node_modules/tar/dist/esm/index.min.js index 57c1c7a4df9a80..bf4036b4115e85 100644 --- a/deps/npm/node_modules/tar/dist/esm/index.min.js +++ b/deps/npm/node_modules/tar/dist/esm/index.min.js @@ -1,4 +1,4 @@ -var Mr=Object.defineProperty;var Br=(s,t)=>{for(var e in t)Mr(s,e,{get:t[e],enumerable:!0})};import $r from"events";import I from"fs";import{EventEmitter as Li}from"node:events";import As from"node:stream";import{StringDecoder as Pr}from"node:string_decoder";var xs=typeof process=="object"&&process?process:{stdout:null,stderr:null},zr=s=>!!s&&typeof s=="object"&&(s instanceof A||s instanceof As||Ur(s)||Hr(s)),Ur=s=>!!s&&typeof s=="object"&&s instanceof Li&&typeof s.pipe=="function"&&s.pipe!==As.Writable.prototype.pipe,Hr=s=>!!s&&typeof s=="object"&&s instanceof Li&&typeof s.write=="function"&&typeof s.end=="function",q=Symbol("EOF"),Q=Symbol("maybeEmitEnd"),rt=Symbol("emittedEnd"),Le=Symbol("emittingEnd"),qt=Symbol("emittedError"),Ne=Symbol("closed"),Ls=Symbol("read"),De=Symbol("flush"),Ns=Symbol("flushChunk"),z=Symbol("encoding"),Mt=Symbol("decoder"),g=Symbol("flowing"),Qt=Symbol("paused"),Bt=Symbol("resume"),b=Symbol("buffer"),D=Symbol("pipes"),_=Symbol("bufferLength"),gi=Symbol("bufferPush"),Ae=Symbol("bufferShift"),L=Symbol("objectMode"),w=Symbol("destroyed"),bi=Symbol("error"),_i=Symbol("emitData"),Ds=Symbol("emitEnd"),Oi=Symbol("emitEnd2"),Z=Symbol("async"),Ti=Symbol("abort"),Ie=Symbol("aborted"),Jt=Symbol("signal"),Rt=Symbol("dataListeners"),F=Symbol("discarded"),jt=s=>Promise.resolve().then(s),Wr=s=>s(),Gr=s=>s==="end"||s==="finish"||s==="prefinish",Zr=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,Yr=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),Fe=class{src;dest;opts;ondrain;constructor(t,e,i){this.src=t,this.dest=e,this.opts=i,this.ondrain=()=>t[Bt](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(t){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},xi=class extends Fe{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(t,e,i){super(t,e,i),this.proxyErrors=r=>this.dest.emit("error",r),t.on("error",this.proxyErrors)}},Kr=s=>!!s.objectMode,Vr=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",A=class extends Li{[g]=!1;[Qt]=!1;[D]=[];[b]=[];[L];[z];[Z];[Mt];[q]=!1;[rt]=!1;[Le]=!1;[Ne]=!1;[qt]=null;[_]=0;[w]=!1;[Jt];[Ie]=!1;[Rt]=0;[F]=!1;writable=!0;readable=!0;constructor(...t){let e=t[0]||{};if(super(),e.objectMode&&typeof e.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Kr(e)?(this[L]=!0,this[z]=null):Vr(e)?(this[z]=e.encoding,this[L]=!1):(this[L]=!1,this[z]=null),this[Z]=!!e.async,this[Mt]=this[z]?new Pr(this[z]):null,e&&e.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[b]}),e&&e.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[D]});let{signal:i}=e;i&&(this[Jt]=i,i.aborted?this[Ti]():i.addEventListener("abort",()=>this[Ti]()))}get bufferLength(){return this[_]}get encoding(){return this[z]}set encoding(t){throw new Error("Encoding must be set at instantiation time")}setEncoding(t){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[L]}set objectMode(t){throw new Error("objectMode must be set at instantiation time")}get async(){return this[Z]}set async(t){this[Z]=this[Z]||!!t}[Ti](){this[Ie]=!0,this.emit("abort",this[Jt]?.reason),this.destroy(this[Jt]?.reason)}get aborted(){return this[Ie]}set aborted(t){}write(t,e,i){if(this[Ie])return!1;if(this[q])throw new Error("write after end");if(this[w])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof e=="function"&&(i=e,e="utf8"),e||(e="utf8");let r=this[Z]?jt:Wr;if(!this[L]&&!Buffer.isBuffer(t)){if(Yr(t))t=Buffer.from(t.buffer,t.byteOffset,t.byteLength);else if(Zr(t))t=Buffer.from(t);else if(typeof t!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[L]?(this[g]&&this[_]!==0&&this[De](!0),this[g]?this.emit("data",t):this[gi](t),this[_]!==0&&this.emit("readable"),i&&r(i),this[g]):t.length?(typeof t=="string"&&!(e===this[z]&&!this[Mt]?.lastNeed)&&(t=Buffer.from(t,e)),Buffer.isBuffer(t)&&this[z]&&(t=this[Mt].write(t)),this[g]&&this[_]!==0&&this[De](!0),this[g]?this.emit("data",t):this[gi](t),this[_]!==0&&this.emit("readable"),i&&r(i),this[g]):(this[_]!==0&&this.emit("readable"),i&&r(i),this[g])}read(t){if(this[w])return null;if(this[F]=!1,this[_]===0||t===0||t&&t>this[_])return this[Q](),null;this[L]&&(t=null),this[b].length>1&&!this[L]&&(this[b]=[this[z]?this[b].join(""):Buffer.concat(this[b],this[_])]);let e=this[Ls](t||null,this[b][0]);return this[Q](),e}[Ls](t,e){if(this[L])this[Ae]();else{let i=e;t===i.length||t===null?this[Ae]():typeof i=="string"?(this[b][0]=i.slice(t),e=i.slice(0,t),this[_]-=t):(this[b][0]=i.subarray(t),e=i.subarray(0,t),this[_]-=t)}return this.emit("data",e),!this[b].length&&!this[q]&&this.emit("drain"),e}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e="utf8"),t!==void 0&&this.write(t,e),i&&this.once("end",i),this[q]=!0,this.writable=!1,(this[g]||!this[Qt])&&this[Q](),this}[Bt](){this[w]||(!this[Rt]&&!this[D].length&&(this[F]=!0),this[Qt]=!1,this[g]=!0,this.emit("resume"),this[b].length?this[De]():this[q]?this[Q]():this.emit("drain"))}resume(){return this[Bt]()}pause(){this[g]=!1,this[Qt]=!0,this[F]=!1}get destroyed(){return this[w]}get flowing(){return this[g]}get paused(){return this[Qt]}[gi](t){this[L]?this[_]+=1:this[_]+=t.length,this[b].push(t)}[Ae](){return this[L]?this[_]-=1:this[_]-=this[b][0].length,this[b].shift()}[De](t=!1){do;while(this[Ns](this[Ae]())&&this[b].length);!t&&!this[b].length&&!this[q]&&this.emit("drain")}[Ns](t){return this.emit("data",t),this[g]}pipe(t,e){if(this[w])return t;this[F]=!1;let i=this[rt];return e=e||{},t===xs.stdout||t===xs.stderr?e.end=!1:e.end=e.end!==!1,e.proxyErrors=!!e.proxyErrors,i?e.end&&t.end():(this[D].push(e.proxyErrors?new xi(this,t,e):new Fe(this,t,e)),this[Z]?jt(()=>this[Bt]()):this[Bt]()),t}unpipe(t){let e=this[D].find(i=>i.dest===t);e&&(this[D].length===1?(this[g]&&this[Rt]===0&&(this[g]=!1),this[D]=[]):this[D].splice(this[D].indexOf(e),1),e.unpipe())}addListener(t,e){return this.on(t,e)}on(t,e){let i=super.on(t,e);if(t==="data")this[F]=!1,this[Rt]++,!this[D].length&&!this[g]&&this[Bt]();else if(t==="readable"&&this[_]!==0)super.emit("readable");else if(Gr(t)&&this[rt])super.emit(t),this.removeAllListeners(t);else if(t==="error"&&this[qt]){let r=e;this[Z]?jt(()=>r.call(this,this[qt])):r.call(this,this[qt])}return i}removeListener(t,e){return this.off(t,e)}off(t,e){let i=super.off(t,e);return t==="data"&&(this[Rt]=this.listeners("data").length,this[Rt]===0&&!this[F]&&!this[D].length&&(this[g]=!1)),i}removeAllListeners(t){let e=super.removeAllListeners(t);return(t==="data"||t===void 0)&&(this[Rt]=0,!this[F]&&!this[D].length&&(this[g]=!1)),e}get emittedEnd(){return this[rt]}[Q](){!this[Le]&&!this[rt]&&!this[w]&&this[b].length===0&&this[q]&&(this[Le]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[Ne]&&this.emit("close"),this[Le]=!1)}emit(t,...e){let i=e[0];if(t!=="error"&&t!=="close"&&t!==w&&this[w])return!1;if(t==="data")return!this[L]&&!i?!1:this[Z]?(jt(()=>this[_i](i)),!0):this[_i](i);if(t==="end")return this[Ds]();if(t==="close"){if(this[Ne]=!0,!this[rt]&&!this[w])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(t==="error"){this[qt]=i,super.emit(bi,i);let n=!this[Jt]||this.listeners("error").length?super.emit("error",i):!1;return this[Q](),n}else if(t==="resume"){let n=super.emit("resume");return this[Q](),n}else if(t==="finish"||t==="prefinish"){let n=super.emit(t);return this.removeAllListeners(t),n}let r=super.emit(t,...e);return this[Q](),r}[_i](t){for(let i of this[D])i.dest.write(t)===!1&&this.pause();let e=this[F]?!1:super.emit("data",t);return this[Q](),e}[Ds](){return this[rt]?!1:(this[rt]=!0,this.readable=!1,this[Z]?(jt(()=>this[Oi]()),!0):this[Oi]())}[Oi](){if(this[Mt]){let e=this[Mt].end();if(e){for(let i of this[D])i.dest.write(e);this[F]||super.emit("data",e)}}for(let e of this[D])e.end();let t=super.emit("end");return this.removeAllListeners("end"),t}async collect(){let t=Object.assign([],{dataLength:0});this[L]||(t.dataLength=0);let e=this.promise();return this.on("data",i=>{t.push(i),this[L]||(t.dataLength+=i.length)}),await e,t}async concat(){if(this[L])throw new Error("cannot concat in objectMode");let t=await this.collect();return this[z]?t.join(""):Buffer.concat(t,t.dataLength)}async promise(){return new Promise((t,e)=>{this.on(w,()=>e(new Error("stream destroyed"))),this.on("error",i=>e(i)),this.on("end",()=>t())})}[Symbol.asyncIterator](){this[F]=!1;let t=!1,e=async()=>(this.pause(),t=!0,{value:void 0,done:!0});return{next:()=>{if(t)return e();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[q])return e();let n,o,a=d=>{this.off("data",h),this.off("end",l),this.off(w,c),e(),o(d)},h=d=>{this.off("error",a),this.off("end",l),this.off(w,c),this.pause(),n({value:d,done:!!this[q]})},l=()=>{this.off("error",a),this.off("data",h),this.off(w,c),e(),n({done:!0,value:void 0})},c=()=>a(new Error("stream destroyed"));return new Promise((d,S)=>{o=S,n=d,this.once(w,c),this.once("error",a),this.once("end",l),this.once("data",h)})},throw:e,return:e,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[F]=!1;let t=!1,e=()=>(this.pause(),this.off(bi,e),this.off(w,e),this.off("end",e),t=!0,{done:!0,value:void 0}),i=()=>{if(t)return e();let r=this.read();return r===null?e():{done:!1,value:r}};return this.once("end",e),this.once(bi,e),this.once(w,e),{next:i,throw:e,return:e,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(t){if(this[w])return t?this.emit("error",t):this.emit(w),this;this[w]=!0,this[F]=!0,this[b].length=0,this[_]=0;let e=this;return typeof e.close=="function"&&!this[Ne]&&e.close(),t?this.emit("error",t):this.emit(w),this}static get isStream(){return zr}};var Xr=I.writev,ot=Symbol("_autoClose"),H=Symbol("_close"),te=Symbol("_ended"),m=Symbol("_fd"),Ni=Symbol("_finished"),j=Symbol("_flags"),Di=Symbol("_flush"),Ci=Symbol("_handleChunk"),ki=Symbol("_makeBuf"),ie=Symbol("_mode"),Ce=Symbol("_needDrain"),Ut=Symbol("_onerror"),Ht=Symbol("_onopen"),Ai=Symbol("_onread"),Pt=Symbol("_onwrite"),ht=Symbol("_open"),U=Symbol("_path"),nt=Symbol("_pos"),Y=Symbol("_queue"),zt=Symbol("_read"),Ii=Symbol("_readSize"),J=Symbol("_reading"),ee=Symbol("_remain"),Fi=Symbol("_size"),ke=Symbol("_write"),gt=Symbol("_writing"),ve=Symbol("_defaultFlag"),bt=Symbol("_errored"),_t=class extends A{[bt]=!1;[m];[U];[Ii];[J]=!1;[Fi];[ee];[ot];constructor(t,e){if(e=e||{},super(e),this.readable=!0,this.writable=!1,typeof t!="string")throw new TypeError("path must be a string");this[bt]=!1,this[m]=typeof e.fd=="number"?e.fd:void 0,this[U]=t,this[Ii]=e.readSize||16*1024*1024,this[J]=!1,this[Fi]=typeof e.size=="number"?e.size:1/0,this[ee]=this[Fi],this[ot]=typeof e.autoClose=="boolean"?e.autoClose:!0,typeof this[m]=="number"?this[zt]():this[ht]()}get fd(){return this[m]}get path(){return this[U]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[ht](){I.open(this[U],"r",(t,e)=>this[Ht](t,e))}[Ht](t,e){t?this[Ut](t):(this[m]=e,this.emit("open",e),this[zt]())}[ki](){return Buffer.allocUnsafe(Math.min(this[Ii],this[ee]))}[zt](){if(!this[J]){this[J]=!0;let t=this[ki]();if(t.length===0)return process.nextTick(()=>this[Ai](null,0,t));I.read(this[m],t,0,t.length,null,(e,i,r)=>this[Ai](e,i,r))}}[Ai](t,e,i){this[J]=!1,t?this[Ut](t):this[Ci](e,i)&&this[zt]()}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}[Ut](t){this[J]=!0,this[H](),this.emit("error",t)}[Ci](t,e){let i=!1;return this[ee]-=t,t>0&&(i=super.write(tthis[Ht](t,e))}[Ht](t,e){this[ve]&&this[j]==="r+"&&t&&t.code==="ENOENT"?(this[j]="w",this[ht]()):t?this[Ut](t):(this[m]=e,this.emit("open",e),this[gt]||this[Di]())}end(t,e){return t&&this.write(t,e),this[te]=!0,!this[gt]&&!this[Y].length&&typeof this[m]=="number"&&this[Pt](null,0),this}write(t,e){return typeof t=="string"&&(t=Buffer.from(t,e)),this[te]?(this.emit("error",new Error("write() after end()")),!1):this[m]===void 0||this[gt]||this[Y].length?(this[Y].push(t),this[Ce]=!0,!1):(this[gt]=!0,this[ke](t),!0)}[ke](t){I.write(this[m],t,0,t.length,this[nt],(e,i)=>this[Pt](e,i))}[Pt](t,e){t?this[Ut](t):(this[nt]!==void 0&&typeof e=="number"&&(this[nt]+=e),this[Y].length?this[Di]():(this[gt]=!1,this[te]&&!this[Ni]?(this[Ni]=!0,this[H](),this.emit("finish")):this[Ce]&&(this[Ce]=!1,this.emit("drain"))))}[Di](){if(this[Y].length===0)this[te]&&this[Pt](null,0);else if(this[Y].length===1)this[ke](this[Y].pop());else{let t=this[Y];this[Y]=[],Xr(this[m],t,this[nt],(e,i)=>this[Pt](e,i))}}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}},Wt=class extends tt{[ht](){let t;if(this[ve]&&this[j]==="r+")try{t=I.openSync(this[U],this[j],this[ie])}catch(e){if(e?.code==="ENOENT")return this[j]="w",this[ht]();throw e}else t=I.openSync(this[U],this[j],this[ie]);this[Ht](null,t)}[H](){if(this[ot]&&typeof this[m]=="number"){let t=this[m];this[m]=void 0,I.closeSync(t),this.emit("close")}}[ke](t){let e=!0;try{this[Pt](null,I.writeSync(this[m],t,0,t.length,this[nt])),e=!1}finally{if(e)try{this[H]()}catch{}}}};import hr from"node:path";import Kt from"node:fs";import{dirname as Nn,parse as Dn}from"path";var qr=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),Is=s=>!!s.sync&&!!s.file,Fs=s=>!s.sync&&!!s.file,Cs=s=>!!s.sync&&!s.file,ks=s=>!s.sync&&!s.file;var vs=s=>!!s.file;var Qr=s=>{let t=qr.get(s);return t||s},se=(s={})=>{if(!s)return{};let t={};for(let[e,i]of Object.entries(s)){let r=Qr(e);t[r]=i}return t.chmod===void 0&&t.noChmod===!1&&(t.chmod=!0),delete t.noChmod,t};var K=(s,t,e,i,r)=>Object.assign((n=[],o,a)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(a=o,o=void 0),o=o?Array.from(o):[];let h=se(n);if(r?.(h,o),Is(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return s(h,o)}else if(Fs(h)){let l=t(h,o);return a?l.then(()=>a(),a):l}else if(Cs(h)){if(typeof a=="function")throw new TypeError("callback not supported for sync tar functions");return e(h,o)}else if(ks(h)){if(typeof a=="function")throw new TypeError("callback only supported with file option");return i(h,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:t,syncNoFile:e,asyncNoFile:i,validate:r});import{EventEmitter as On}from"events";import Pi from"assert";import{Buffer as Ot}from"buffer";import*as Ms from"zlib";import Jr from"zlib";var jr=Jr.constants||{ZLIB_VERNUM:4736},M=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},jr));var tn=Ot.concat,Bs=Object.getOwnPropertyDescriptor(Ot,"concat"),en=s=>s,Mi=Bs?.writable===!0||Bs?.set!==void 0?s=>{Ot.concat=s?en:tn}:s=>{},Tt=Symbol("_superWrite"),Gt=class extends Error{code;errno;constructor(t,e){super("zlib: "+t.message,{cause:t}),this.code=t.code,this.errno=t.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+t.message,Error.captureStackTrace(this,e??this.constructor)}get name(){return"ZlibError"}},Bi=Symbol("flushFlag"),re=class extends A{#t=!1;#i=!1;#s;#n;#r;#e;#o;get sawError(){return this.#t}get handle(){return this.#e}get flushFlag(){return this.#s}constructor(t,e){if(!t||typeof t!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(t),this.#s=t.flush??0,this.#n=t.finishFlush??0,this.#r=t.fullFlushFlag??0,typeof Ms[e]!="function")throw new TypeError("Compression method not supported: "+e);try{this.#e=new Ms[e](t)}catch(i){throw new Gt(i,this.constructor)}this.#o=i=>{this.#t||(this.#t=!0,this.close(),this.emit("error",i))},this.#e?.on("error",i=>this.#o(new Gt(i))),this.once("end",()=>this.close)}close(){this.#e&&(this.#e.close(),this.#e=void 0,this.emit("close"))}reset(){if(!this.#t)return Pi(this.#e,"zlib binding closed"),this.#e.reset?.()}flush(t){this.ended||(typeof t!="number"&&(t=this.#r),this.write(Object.assign(Ot.alloc(0),{[Bi]:t})))}end(t,e,i){return typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&(e?this.write(t,e):this.write(t)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Tt](t){return super.write(t)}write(t,e,i){if(typeof e=="function"&&(i=e,e="utf8"),typeof t=="string"&&(t=Ot.from(t,e)),this.#t)return;Pi(this.#e,"zlib binding closed");let r=this.#e._handle,n=r.close;r.close=()=>{};let o=this.#e.close;this.#e.close=()=>{},Mi(!0);let a;try{let l=typeof t[Bi]=="number"?t[Bi]:this.#s;a=this.#e._processChunk(t,l),Mi(!1)}catch(l){Mi(!1),this.#o(new Gt(l,this.write))}finally{this.#e&&(this.#e._handle=r,r.close=n,this.#e.close=o,this.#e.removeAllListeners("error"))}this.#e&&this.#e.on("error",l=>this.#o(new Gt(l,this.write)));let h;if(a)if(Array.isArray(a)&&a.length>0){let l=a[0];h=this[Tt](Ot.from(l));for(let c=1;c{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(t,e)}finally{this.handle.flush=i}this.handle&&(this.#t=t,this.#i=e)}}}};var Pe=class extends Be{#t;constructor(t){super(t,"Gzip"),this.#t=t&&!!t.portable}[Tt](t){return this.#t?(this.#t=!1,t[9]=255,super[Tt](t)):super[Tt](t)}};var ze=class extends Be{constructor(t){super(t,"Unzip")}},Ue=class extends re{constructor(t,e){t=t||{},t.flush=t.flush||M.BROTLI_OPERATION_PROCESS,t.finishFlush=t.finishFlush||M.BROTLI_OPERATION_FINISH,t.fullFlushFlag=M.BROTLI_OPERATION_FLUSH,super(t,e)}},He=class extends Ue{constructor(t){super(t,"BrotliCompress")}},We=class extends Ue{constructor(t){super(t,"BrotliDecompress")}},Ge=class extends re{constructor(t,e){t=t||{},t.flush=t.flush||M.ZSTD_e_continue,t.finishFlush=t.finishFlush||M.ZSTD_e_end,t.fullFlushFlag=M.ZSTD_e_flush,super(t,e)}},Ze=class extends Ge{constructor(t){super(t,"ZstdCompress")}},Ye=class extends Ge{constructor(t){super(t,"ZstdDecompress")}};import{posix as Zt}from"node:path";var Ps=(s,t)=>{if(Number.isSafeInteger(s))s<0?nn(s,t):rn(s,t);else throw Error("cannot encode number outside of javascript safe integer range");return t},rn=(s,t)=>{t[0]=128;for(var e=t.length;e>1;e--)t[e-1]=s&255,s=Math.floor(s/256)},nn=(s,t)=>{t[0]=255;var e=!1;s=s*-1;for(var i=t.length;i>1;i--){var r=s&255;s=Math.floor(s/256),e?t[i-1]=Us(r):r===0?t[i-1]=0:(e=!0,t[i-1]=Hs(r))}},zs=s=>{let t=s[0],e=t===128?hn(s.subarray(1,s.length)):t===255?on(s):null;if(e===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(e))throw Error("parsed number outside of javascript safe integer range");return e},on=s=>{for(var t=s.length,e=0,i=!1,r=t-1;r>-1;r--){var n=Number(s[r]),o;i?o=Us(n):n===0?o=n:(i=!0,o=Hs(n)),o!==0&&(e-=o*Math.pow(256,t-r-1))}return e},hn=s=>{for(var t=s.length,e=0,i=t-1;i>-1;i--){var r=Number(s[i]);r!==0&&(e+=r*Math.pow(256,t-i-1))}return e},Us=s=>(255^s)&255,Hs=s=>(255^s)+1&255;var Ui={};Br(Ui,{code:()=>Ke,isCode:()=>ne,isName:()=>ln,name:()=>oe,normalFsTypes:()=>zi});var ne=s=>oe.has(s),ln=s=>Ke.has(s),zi=new Set(["0","","1","2","3","4","5","6","7","D"]),oe=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]),Ke=new Map(Array.from(oe).map(s=>[s[1],s[0]]));var C=class{cksumValid=!1;needPax=!1;nullBlock=!1;block;path;mode;uid;gid;size;cksum;#t="Unsupported";linkpath;uname;gname;devmaj=0;devmin=0;atime;ctime;mtime;charset;comment;constructor(t,e=0,i,r){Buffer.isBuffer(t)?this.decode(t,e||0,i,r):t&&this.#i(t)}decode(t,e,i,r){if(e||(e=0),!t||!(t.length>=e+512))throw new Error("need 512 bytes for header");let n=xt(t,e+156,1),o=zi.has(n),a=o?i:void 0,h=o?r:void 0;if(this.path=a?.path??xt(t,e,100),this.mode=a?.mode??h?.mode??at(t,e+100,8),this.uid=a?.uid??h?.uid??at(t,e+108,8),this.gid=a?.gid??h?.gid??at(t,e+116,8),this.size=a?.size??h?.size??at(t,e+124,12),this.mtime=a?.mtime??h?.mtime??Hi(t,e+136,12),this.cksum=at(t,e+148,12),h&&this.#i(h,!0),a&&this.#i(a),ne(n)&&(this.#t=n||"0"),this.#t==="0"&&this.path.slice(-1)==="/"&&(this.#t="5"),this.#t==="5"&&(this.size=0),this.linkpath=xt(t,e+157,100),t.subarray(e+257,e+265).toString()==="ustar\x0000")if(this.uname=a?.uname??h?.uname??xt(t,e+265,32),this.gname=a?.gname??h?.gname??xt(t,e+297,32),this.devmaj=a?.devmaj??h?.devmaj??at(t,e+329,8)??0,this.devmin=a?.devmin??h?.devmin??at(t,e+337,8)??0,t[e+475]!==0){let c=xt(t,e+345,155);this.path=c+"/"+this.path}else{let c=xt(t,e+345,130);c&&(this.path=c+"/"+this.path),this.atime=i?.atime??r?.atime??Hi(t,e+476,12),this.ctime=i?.ctime??r?.ctime??Hi(t,e+488,12)}let l=256;for(let c=e;c!(r==null||i==="path"&&e||i==="linkpath"&&e||i==="global"))))}encode(t,e=0){if(t||(t=this.block=Buffer.alloc(512)),this.#t==="Unsupported"&&(this.#t="0"),!(t.length>=e+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=cn(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=Lt(t,e,100,n)||this.needPax,this.needPax=lt(t,e+100,8,this.mode)||this.needPax,this.needPax=lt(t,e+108,8,this.uid)||this.needPax,this.needPax=lt(t,e+116,8,this.gid)||this.needPax,this.needPax=lt(t,e+124,12,this.size)||this.needPax,this.needPax=Wi(t,e+136,12,this.mtime)||this.needPax,t[e+156]=Number(this.#t.codePointAt(0)),this.needPax=Lt(t,e+157,100,this.linkpath)||this.needPax,t.write("ustar\x0000",e+257,8),this.needPax=Lt(t,e+265,32,this.uname)||this.needPax,this.needPax=Lt(t,e+297,32,this.gname)||this.needPax,this.needPax=lt(t,e+329,8,this.devmaj)||this.needPax,this.needPax=lt(t,e+337,8,this.devmin)||this.needPax,this.needPax=Lt(t,e+345,i,o)||this.needPax,t[e+475]!==0?this.needPax=Lt(t,e+345,155,o)||this.needPax:(this.needPax=Lt(t,e+345,130,o)||this.needPax,this.needPax=Wi(t,e+476,12,this.atime)||this.needPax,this.needPax=Wi(t,e+488,12,this.ctime)||this.needPax);let a=256;for(let h=e;h{let i=s,r="",n,o=Zt.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Zt.dirname(i),i=Zt.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=t?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=t?n=[i.slice(0,99),r,!0]:(i=Zt.join(Zt.basename(r),i),r=Zt.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},xt=(s,t,e)=>s.subarray(t,t+e).toString("utf8").replace(/\0.*/,""),Hi=(s,t,e)=>fn(at(s,t,e)),fn=s=>s===void 0?void 0:new Date(s*1e3),at=(s,t,e)=>Number(s[t])&128?zs(s.subarray(t,t+e)):un(s,t,e),dn=s=>isNaN(s)?void 0:s,un=(s,t,e)=>dn(parseInt(s.subarray(t,t+e).toString("utf8").replace(/\0.*$/,"").trim(),8)),mn={12:8589934591,8:2097151},lt=(s,t,e,i)=>i===void 0?!1:i>mn[e]||i<0?(Ps(i,s.subarray(t,t+e)),!0):(pn(s,t,e,i),!1),pn=(s,t,e,i)=>s.write(En(i,e),t,e,"ascii"),En=(s,t)=>wn(Math.floor(s).toString(8),t),wn=(s,t)=>(s.length===t-1?s:new Array(t-s.length-1).join("0")+s+" ")+"\0",Wi=(s,t,e,i)=>i===void 0?!1:lt(s,t,e,i.getTime()/1e3),Sn=new Array(156).join("\0"),Lt=(s,t,e,i)=>i===void 0?!1:(s.write(i+Sn,t,e,"utf8"),i.length!==Buffer.byteLength(i)||i.length>e);import{basename as yn}from"node:path";var ct=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(t,e=!1){this.atime=t.atime,this.charset=t.charset,this.comment=t.comment,this.ctime=t.ctime,this.dev=t.dev,this.gid=t.gid,this.global=e,this.gname=t.gname,this.ino=t.ino,this.linkpath=t.linkpath,this.mtime=t.mtime,this.nlink=t.nlink,this.path=t.path,this.size=t.size,this.uid=t.uid,this.uname=t.uname}encode(){let t=this.encodeBody();if(t==="")return Buffer.allocUnsafe(0);let e=Buffer.byteLength(t),i=512*Math.ceil(1+e/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new C({path:("PaxHeader/"+yn(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:e,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(t,512,e,"utf8");for(let n=e+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(t,e,i=!1){return new s(Rn(gn(t),e),i)}},Rn=(s,t)=>t?Object.assign({},t,s):s,gn=s=>s.replace(/\n$/,"").split(` -`).reduce(bn,Object.create(null)),bn=(s,t)=>{let e=parseInt(t,10);if(e!==Buffer.byteLength(t)+1)return s;t=t.slice((e+" ").length);let i=t.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=");return s[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(o)*1e3):/^[0-9]+$/.test(o)?+o:o,s};var _n=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,f=_n!=="win32"?s=>s:s=>s&&s.replaceAll(/\\/g,"/");var Ve=class extends A{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(t,e,i){switch(super({}),this.pause(),this.extended=e,this.globalExtended=i,this.header=t,this.remain=t.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=t.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!t.path)throw new Error("no path provided for tar.ReadEntry");this.path=f(t.path),this.mode=t.mode,this.mode&&(this.mode=this.mode&4095),this.uid=t.uid,this.gid=t.gid,this.uname=t.uname,this.gname=t.gname,this.size=this.remain,this.mtime=t.mtime,this.atime=t.atime,this.ctime=t.ctime,this.linkpath=t.linkpath?f(t.linkpath):void 0,this.uname=t.uname,this.gname=t.gname,e&&this.#t(e),i&&this.#t(i,!0)}write(t){let e=t.length;if(e>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-e),this.blockRemain=Math.max(0,r-e),this.ignore?!0:i>=e?super.write(t):super.write(t.subarray(0,i))}#t(t,e=!1){t.path&&(t.path=f(t.path)),t.linkpath&&(t.linkpath=f(t.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(t).filter(([i,r])=>!(r==null||i==="path"&&e))))}};var Nt=(s,t,e,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=e instanceof Error&&e.code||t,i.tarCode=t,!s.strict&&i.recoverable!==!1?(e instanceof Error&&(i=Object.assign(e,i),e=e.message),s.emit("warn",t,e,i)):e instanceof Error?s.emit("error",Object.assign(e,i)):s.emit("error",Object.assign(new Error(`${t}: ${e}`),i))};var Tn=1024*1024,Vi=Buffer.from([31,139]),$i=Buffer.from([40,181,47,253]),xn=Math.max(Vi.length,$i.length),B=Symbol("state"),Dt=Symbol("writeEntry"),et=Symbol("readEntry"),Gi=Symbol("nextEntry"),Ws=Symbol("processEntry"),V=Symbol("extendedHeader"),he=Symbol("globalExtendedHeader"),ft=Symbol("meta"),Gs=Symbol("emitMeta"),p=Symbol("buffer"),it=Symbol("queue"),dt=Symbol("ended"),Zi=Symbol("emittedEnd"),At=Symbol("emit"),y=Symbol("unzip"),$e=Symbol("consumeChunk"),Xe=Symbol("consumeChunkSub"),Yi=Symbol("consumeBody"),Zs=Symbol("consumeMeta"),Ys=Symbol("consumeHeader"),ae=Symbol("consuming"),Ki=Symbol("bufferConcat"),qe=Symbol("maybeEnd"),Yt=Symbol("writing"),ut=Symbol("aborted"),Qe=Symbol("onDone"),It=Symbol("sawValidEntry"),Je=Symbol("sawNullBlock"),je=Symbol("sawEOF"),Ks=Symbol("closeStream"),Ln=()=>!0,st=class extends On{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[it]=[];[p];[et];[Dt];[B]="begin";[ft]="";[V];[he];[dt]=!1;[y];[ut]=!1;[It];[Je]=!1;[je]=!1;[Yt]=!1;[ae]=!1;[Zi]=!1;constructor(t={}){super(),this.file=t.file||"",this.on(Qe,()=>{(this[B]==="begin"||this[It]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),t.ondone?this.on(Qe,t.ondone):this.on(Qe,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!t.strict,this.maxMetaEntrySize=t.maxMetaEntrySize||Tn,this.filter=typeof t.filter=="function"?t.filter:Ln;let e=t.file&&(t.file.endsWith(".tar.br")||t.file.endsWith(".tbr"));this.brotli=!(t.gzip||t.zstd)&&t.brotli!==void 0?t.brotli:e?void 0:!1;let i=t.file&&(t.file.endsWith(".tar.zst")||t.file.endsWith(".tzst"));this.zstd=!(t.gzip||t.brotli)&&t.zstd!==void 0?t.zstd:i?!0:void 0,this.on("end",()=>this[Ks]()),typeof t.onwarn=="function"&&this.on("warn",t.onwarn),typeof t.onReadEntry=="function"&&this.on("entry",t.onReadEntry)}warn(t,e,i={}){Nt(this,t,e,i)}[Ys](t,e){this[It]===void 0&&(this[It]=!1);let i;try{i=new C(t,e,this[V],this[he])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[Je]?(this[je]=!0,this[B]==="begin"&&(this[B]="header"),this[At]("eof")):(this[Je]=!0,this[At]("nullBlock"));else if(this[Je]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[Dt]=new Ve(i,this[V],this[he]);if(!this[It])if(n.remain){let o=()=>{n.invalid||(this[It]=!0)};n.on("end",o)}else this[It]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[At]("ignoredEntry",n),this[B]="ignore",n.resume()):n.size>0&&(this[ft]="",n.on("data",o=>this[ft]+=o),this[B]="meta"):(this[V]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[At]("ignoredEntry",n),this[B]=n.remain?"ignore":"header",n.resume()):(n.remain?this[B]="body":(this[B]="header",n.end()),this[et]?this[it].push(n):(this[it].push(n),this[Gi]())))}}}[Ks](){queueMicrotask(()=>this.emit("close"))}[Ws](t){let e=!0;if(!t)this[et]=void 0,e=!1;else if(Array.isArray(t)){let[i,...r]=t;this.emit(i,...r)}else this[et]=t,this.emit("entry",t),t.emittedEnd||(t.on("end",()=>this[Gi]()),e=!1);return e}[Gi](){do;while(this[Ws](this[it].shift()));if(this[it].length===0){let t=this[et];!t||t.flowing||t.size===t.remain?this[Yt]||this.emit("drain"):t.once("drain",()=>this.emit("drain"))}}[Yi](t,e){let i=this[Dt];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=t.length&&e===0?t:t.subarray(e,e+r);return i.write(n),i.blockRemain||(this[B]="header",this[Dt]=void 0,i.end()),n.length}[Zs](t,e){let i=this[Dt],r=this[Yi](t,e);return!this[Dt]&&i&&this[Gs](i),r}[At](t,e,i){this[it].length===0&&!this[et]?this.emit(t,e,i):this[it].push([t,e,i])}[Gs](t){switch(this[At]("meta",this[ft]),t.type){case"ExtendedHeader":case"OldExtendedHeader":this[V]=ct.parse(this[ft],this[V],!1);break;case"GlobalExtendedHeader":this[he]=ct.parse(this[ft],this[he],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let e=this[V]??Object.create(null);this[V]=e,e.path=this[ft].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let e=this[V]||Object.create(null);this[V]=e,e.linkpath=this[ft].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+t.type)}}abort(t){this[ut]=!0,this.emit("abort",t),this.warn("TAR_ABORT",t,{recoverable:!1})}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this[ut])return i?.(),!1;if((this[y]===void 0||this.brotli===void 0&&this[y]===!1)&&t){if(this[p]&&(t=Buffer.concat([this[p],t]),this[p]=void 0),t.lengththis[$e](c)),this[y].on("error",c=>this.abort(c)),this[y].on("end",()=>{this[dt]=!0,this[$e]()}),this[Yt]=!0;let l=!!this[y][h?"end":"write"](t);return this[Yt]=!1,i?.(),l}}this[Yt]=!0,this[y]?this[y].write(t):this[$e](t),this[Yt]=!1;let n=this[it].length>0?!1:this[et]?this[et].flowing:!0;return!n&&this[it].length===0&&this[et]?.once("drain",()=>this.emit("drain")),i?.(),n}[Ki](t){t&&!this[ut]&&(this[p]=this[p]?Buffer.concat([this[p],t]):t)}[qe](){if(this[dt]&&!this[Zi]&&!this[ut]&&!this[ae]){this[Zi]=!0;let t=this[Dt];if(t&&t.blockRemain){let e=this[p]?this[p].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${t.blockRemain} more bytes, only ${e} available)`,{entry:t}),this[p]&&t.write(this[p]),t.end()}this[At](Qe)}}[$e](t){if(this[ae]&&t)this[Ki](t);else if(!t&&!this[p])this[qe]();else if(t){if(this[ae]=!0,this[p]){this[Ki](t);let e=this[p];this[p]=void 0,this[Xe](e)}else this[Xe](t);for(;this[p]&&this[p]?.length>=512&&!this[ut]&&!this[je];){let e=this[p];this[p]=void 0,this[Xe](e)}this[ae]=!1}(!this[p]||this[dt])&&this[qe]()}[Xe](t){let e=0,i=t.length;for(;e+512<=i&&!this[ut]&&!this[je];)switch(this[B]){case"begin":case"header":this[Ys](t,e),e+=512;break;case"ignore":case"body":e+=this[Yi](t,e);break;case"meta":e+=this[Zs](t,e);break;default:throw new Error("invalid state: "+this[B])}e{let t=s.length-1,e=-1;for(;t>-1&&s.charAt(t)==="/";)e=t,t--;return e===-1?s:s.slice(0,e)};var An=s=>{let t=s.onReadEntry;s.onReadEntry=t?e=>{t(e),e.resume()}:e=>e.resume()},Xi=(s,t)=>{let e=new Map(t.map(n=>[mt(n),!0])),i=s.filter,r=(n,o="")=>{let a=o||Dn(n).root||".",h;if(n===a)h=!1;else{let l=e.get(n);h=l!==void 0?l:r(Nn(n),a)}return e.set(n,h),h};s.filter=i?(n,o)=>i(n,o)&&r(mt(n)):n=>r(mt(n))},In=s=>{let t=new st(s),e=s.file,i;try{i=Kt.openSync(e,"r");let r=Kt.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let e=new st(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{e.on("error",a),e.on("end",o),Kt.stat(r,(h,l)=>{if(h)a(h);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",a),c.pipe(e)}})})},Ft=K(In,Fn,s=>new st(s),s=>new st(s),(s,t)=>{t?.length&&Xi(s,t),s.noResume||An(s)});import ui from"fs";import $ from"fs";import qs from"path";var qi=(s,t,e)=>(s&=4095,e&&(s=(s|384)&-19),t&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);import{win32 as Cn}from"node:path";var{isAbsolute:kn,parse:Vs}=Cn,le=s=>{let t="",e=Vs(s);for(;kn(s)||e.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":e.root;s=s.slice(i.length),t+=i,e=Vs(s)}return[t,s]};var ti=["|","<",">","?",":"],Qi=ti.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),vn=new Map(ti.map((s,t)=>[s,Qi[t]])),Mn=new Map(Qi.map((s,t)=>[s,ti[t]])),Ji=s=>ti.reduce((t,e)=>t.split(e).join(vn.get(e)),s),$s=s=>Qi.reduce((t,e)=>t.split(e).join(Mn.get(e)),s);var er=(s,t)=>t?(s=f(s).replace(/^\.(\/|$)/,""),mt(t)+"/"+s):f(s),Bn=16*1024*1024,Qs=Symbol("process"),Js=Symbol("file"),js=Symbol("directory"),ts=Symbol("symlink"),tr=Symbol("hardlink"),ce=Symbol("header"),ei=Symbol("read"),es=Symbol("lstat"),ii=Symbol("onlstat"),is=Symbol("onread"),ss=Symbol("onreadlink"),rs=Symbol("openfile"),ns=Symbol("onopenfile"),pt=Symbol("close"),si=Symbol("mode"),os=Symbol("awaitDrain"),ji=Symbol("ondrain"),X=Symbol("prefix"),fe=class extends A{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#t=!1;constructor(t,e={}){let i=se(e);super(),this.path=f(t),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||Bn,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=f(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?f(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,a]=le(this.path);o&&typeof a=="string"&&(this.path=a,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=$s(this.path.replaceAll(/\\/g,"/")),t=t.replaceAll(/\\/g,"/")),this.absolute=f(i.absolute||qs.resolve(this.cwd,t)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[ii](n):this[es]()}warn(t,e,i={}){return Nt(this,t,e,i)}emit(t,...e){return t==="error"&&(this.#t=!0),super.emit(t,...e)}[es](){$.lstat(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[ii](e)})}[ii](t){this.statCache.set(this.absolute,t),this.stat=t,t.isFile()||(t.size=0),this.type=Pn(t),this.emit("stat",t),this[Qs]()}[Qs](){switch(this.type){case"File":return this[Js]();case"Directory":return this[js]();case"SymbolicLink":return this[ts]();default:return this.end()}}[si](t){return qi(t,this.type==="Directory",this.portable)}[X](t){return er(t,this.prefix)}[ce](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new C({path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,mode:this[si](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new ct({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let t=this.header?.block;if(!t)throw new Error("failed to encode header");super.write(t)}[js](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[ce](),this.end()}[ts](){$.readlink(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[ss](e)})}[ss](t){this.linkpath=f(t),this[ce](),this.end()}[tr](t){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=f(qs.relative(this.cwd,t)),this.stat.size=0,this[ce](),this.end()}[Js](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let t=`${this.stat.dev}:${this.stat.ino}`,e=this.linkCache.get(t);if(e?.indexOf(this.cwd)===0)return this[tr](e);this.linkCache.set(t,this.absolute)}if(this[ce](),this.stat.size===0)return this.end();this[rs]()}[rs](){$.open(this.absolute,"r",(t,e)=>{if(t)return this.emit("error",t);this[ns](e)})}[ns](t){if(this.fd=t,this.#t)return this[pt]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let e=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(e),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[ei]()}[ei](){let{fd:t,buf:e,offset:i,length:r,pos:n}=this;if(t===void 0||e===void 0)throw new Error("cannot read file without first opening");$.read(t,e,i,r,n,(o,a)=>{if(o)return this[pt](()=>this.emit("error",o));this[is](a)})}[pt](t=()=>{}){this.fd!==void 0&&$.close(this.fd,t)}[is](t){if(t<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(t>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(t===this.remain)for(let r=t;rthis[ji]())}[os](t){this.once("drain",t)}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this.blockRemaint?this.emit("error",t):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[ei]()}},ri=class extends fe{sync=!0;[es](){this[ii]($.lstatSync(this.absolute))}[ts](){this[ss]($.readlinkSync(this.absolute))}[rs](){this[ns]($.openSync(this.absolute,"r"))}[ei](){let t=!0;try{let{fd:e,buf:i,offset:r,length:n,pos:o}=this;if(e===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let a=$.readSync(e,i,r,n,o);this[is](a),t=!1}finally{if(t)try{this[pt](()=>{})}catch{}}}[os](t){t()}[pt](t=()=>{}){this.fd!==void 0&&$.closeSync(this.fd),t()}},ni=class extends A{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(t,e,i={}){return Nt(this,t,e,i)}constructor(t,e={}){let i=se(e);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=t;let{type:r}=t;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=f(t.path),this.mode=t.mode!==void 0?this[si](t.mode):void 0,this.uid=this.portable?void 0:t.uid,this.gid=this.portable?void 0:t.gid,this.uname=this.portable?void 0:t.uname,this.gname=this.portable?void 0:t.gname,this.size=t.size,this.mtime=this.noMtime?void 0:i.mtime||t.mtime,this.atime=this.portable?void 0:t.atime,this.ctime=this.portable?void 0:t.ctime,this.linkpath=t.linkpath!==void 0?f(t.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[a,h]=le(this.path);a&&typeof h=="string"&&(this.path=h,n=a)}this.remain=t.size,this.blockRemain=t.startBlockSize,this.onWriteEntry?.(this),this.header=new C({path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new ct({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[X](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[X](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),t.pipe(this)}[X](t){return er(t,this.prefix)}[si](t){return qi(t,this.type==="Directory",this.portable)}write(t,e,i){typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8"));let r=t.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(t,i)}end(t,e,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,e??"utf8")),i&&this.once("finish",i),t?super.end(t,i):super.end(i),this}},Pn=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported";var oi=class s{tail;head;length=0;static create(t=[]){return new s(t)}constructor(t=[]){for(let e of t)this.push(e)}*[Symbol.iterator](){for(let t=this.head;t;t=t.next)yield t.value}removeNode(t){if(t.list!==this)throw new Error("removing node which does not belong to this list");let e=t.next,i=t.prev;return e&&(e.prev=i),i&&(i.next=e),t===this.head&&(this.head=e),t===this.tail&&(this.tail=i),this.length--,t.next=void 0,t.prev=void 0,t.list=void 0,e}unshiftNode(t){if(t===this.head)return;t.list&&t.list.removeNode(t);let e=this.head;t.list=this,t.next=e,e&&(e.prev=t),this.head=t,this.tail||(this.tail=t),this.length++}pushNode(t){if(t===this.tail)return;t.list&&t.list.removeNode(t);let e=this.tail;t.list=this,t.prev=e,e&&(e.next=t),this.tail=t,this.head||(this.head=t),this.length++}push(...t){for(let e=0,i=t.length;e1)i=e;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=t(i,r.value,n),r=r.next;return i}reduceReverse(t,e){let i,r=this.tail;if(arguments.length>1)i=e;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=t(i,r.value,n),r=r.prev;return i}toArray(){let t=new Array(this.length);for(let e=0,i=this.head;i;e++)t[e]=i.value,i=i.next;return t}toArrayReverse(){let t=new Array(this.length);for(let e=0,i=this.tail;i;e++)t[e]=i.value,i=i.prev;return t}slice(t=0,e=this.length){e<0&&(e+=this.length),t<0&&(t+=this.length);let i=new s;if(ethis.length&&(e=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(e=this.length);let r=this.length,n=this.tail;for(;n&&r>e;r--)n=n.prev;for(;n&&r>t;r--,n=n.prev)i.push(n.value);return i}splice(t,e=0,...i){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);let r=this.head;for(let o=0;r&&o1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(t.gzip&&(typeof t.gzip!="object"&&(t.gzip={}),this.portable&&(t.gzip.portable=!0),this.zip=new Pe(t.gzip)),t.brotli&&(typeof t.brotli!="object"&&(t.brotli={}),this.zip=new He(t.brotli)),t.zstd&&(typeof t.zstd!="object"&&(t.zstd={}),this.zip=new Ze(t.zstd)),!this.zip)throw new Error("impossible");let e=this.zip;e.on("data",i=>super.write(i)),e.on("end",()=>super.end()),e.on("drain",()=>this[ls]()),this.on("resume",()=>e.resume())}else this.on("drain",this[ls]);this.noDirRecurse=!!t.noDirRecurse,this.follow=!!t.follow,this.noMtime=!!t.noMtime,t.mtime&&(this.mtime=t.mtime),this.filter=typeof t.filter=="function"?t.filter:()=>!0,this[W]=new oi,this[G]=0,this.jobs=Number(t.jobs)||4,this[pe]=!1,this[ue]=!1}[or](t){return super.write(t)}add(t){return this.write(t),this}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&this.add(t),this[ue]=!0,this[Ct](),i&&i(),this}write(t){if(this[ue])throw new Error("write after end");return typeof t=="string"?this[li](t):this[sr](t),this.flowing}[sr](t){let e=f(nr.resolve(this.cwd,t.path));if(!this.filter(t.path,t))t.resume();else{let i=new mi(t.path,e);i.entry=new ni(t,this[as](i)),i.entry.on("end",()=>this[hs](i)),this[G]+=1,this[W].push(i)}this[Ct]()}[li](t){let e=f(nr.resolve(this.cwd,t));this[W].push(new mi(t,e)),this[Ct]()}[cs](t){t.pending=!0,this[G]+=1;let e=this.follow?"stat":"lstat";ui[e](t.absolute,(i,r)=>{t.pending=!1,this[G]-=1,i?this.emit("error",i):this[ai](t,r)})}[ai](t,e){if(this.statCache.set(t.absolute,e),t.stat=e,!this.filter(t.path,e))t.ignore=!0;else if(e.isFile()&&e.nlink>1&&!this.linkCache.get(`${e.dev}:${e.ino}`)&&!this.sync)if(t===this[Et])this[hi](t);else{let i=`${e.dev}:${e.ino}`,r=this[me].get(i);r?r.push(t):this[me].set(i,[t]),t.pendingLink=!0,t.pending=!0}this[Ct]()}[fs](t){t.pending=!0,this[G]+=1,ui.readdir(t.absolute,(e,i)=>{if(t.pending=!1,this[G]-=1,e)return this.emit("error",e);this[ci](t,i)})}[ci](t,e){this.readdirCache.set(t.absolute,e),t.readdir=e,this[Ct]()}[Ct](){if(!this[pe]){this[pe]=!0;for(let t=this[W].head;t&&this[G]1){let i=`${e.dev}:${e.ino}`,r=this[me].get(i);if(r){this[me].delete(i);for(let n of r)n.pending=!1,this[hi](n)}}this[Ct]()}[hi](t){if(t.pending&&t.pendingLink&&t===this[Et]&&(t.pending=!1,t.pendingLink=!1),!t.pending){if(t.entry){t===this[Et]&&!t.piped&&this[fi](t);return}if(!t.stat){let e=this.statCache.get(t.absolute);e?this[ai](t,e):this[cs](t)}if(t.stat&&!t.ignore){if(!this.noDirRecurse&&t.stat.isDirectory()&&!t.readdir){let e=this.readdirCache.get(t.absolute);if(e?this[ci](t,e):this[fs](t),!t.readdir)return}if(t.entry=this[rr](t),!t.entry){t.ignore=!0;return}t===this[Et]&&!t.piped&&this[fi](t)}}}[as](t){return{onwarn:(e,i,r)=>this.warn(e,i,r),noPax:this.noPax,cwd:this.cwd,absolute:t.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[rr](t){this[G]+=1;try{return new this[di](t.path,this[as](t)).on("end",()=>this[hs](t)).on("error",i=>this.emit("error",i))}catch(e){this.emit("error",e)}}[ls](){this[Et]&&this[Et].entry&&this[Et].entry.resume()}[fi](t){t.piped=!0,t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[li](o+r)});let e=t.entry,i=this.zip;if(!e)throw new Error("cannot pipe without source");i?e.on("data",r=>{i.write(r)||e.pause()}):e.on("data",r=>{super.write(r)||e.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(t,e,i={}){Nt(this,t,e,i)}},kt=class extends wt{sync=!0;constructor(t){super(t),this[di]=ri}pause(){}resume(){}[cs](t){let e=this.follow?"statSync":"lstatSync";this[ai](t,ui[e](t.absolute))}[fs](t){this[ci](t,ui.readdirSync(t.absolute))}[fi](t){let e=t.entry,i=this.zip;if(t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[li](o+r)}),!e)throw new Error("Cannot pipe without source");i?e.on("data",r=>{i.write(r)}):e.on("data",r=>{super[or](r)})}};var Wn=(s,t)=>{let e=new kt(s),i=new Wt(s.file,{mode:s.mode||438});e.pipe(i),ar(e,t)},Gn=(s,t)=>{let e=new wt(s),i=new tt(s.file,{mode:s.mode||438});e.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),e.on("error",o)});return lr(e,t).catch(n=>e.emit("error",n)),r},ar=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?Ft({file:hr.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},lr=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await Ft({file:hr.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(e);s.end()},Zn=(s,t)=>{let e=new kt(s);return ar(e,t),e},Yn=(s,t)=>{let e=new wt(s);return lr(e,t).catch(i=>e.emit("error",i)),e},Kn=K(Wn,Gn,Zn,Yn,(s,t)=>{if(!t?.length)throw new TypeError("no paths specified to add to archive")});import kr from"node:fs";import no from"node:assert";import{randomBytes as Cr}from"node:crypto";import u from"node:fs";import R from"node:path";import dr from"fs";var Vn=process.env.__FAKE_PLATFORM__||process.platform,ur=Vn==="win32",{O_CREAT:mr,O_NOFOLLOW:cr,O_TRUNC:pr,O_WRONLY:Er}=dr.constants,wr=Number(process.env.__FAKE_FS_O_FILENAME__)||dr.constants.UV_FS_O_FILEMAP||0,$n=ur&&!!wr,Xn=512*1024,qn=wr|pr|mr|Er,fr=!ur&&typeof cr=="number"?cr|pr|mr|Er:null,ds=fr!==null?()=>fr:$n?s=>s"w";import Ei from"node:fs";import Ee from"node:path";var us=(s,t,e)=>{try{return Ei.lchownSync(s,t,e)}catch(i){if(i?.code!=="ENOENT")throw i}},pi=(s,t,e,i)=>{Ei.lchown(s,t,e,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},Qn=(s,t,e,i,r)=>{if(t.isDirectory())ms(Ee.resolve(s,t.name),e,i,n=>{if(n)return r(n);let o=Ee.resolve(s,t.name);pi(o,e,i,r)});else{let n=Ee.resolve(s,t.name);pi(n,e,i,r)}},ms=(s,t,e,i)=>{Ei.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return pi(s,t,e,i);let o=n.length,a=null,h=l=>{if(!a){if(l)return i(a=l);if(--o===0)return pi(s,t,e,i)}};for(let l of n)Qn(s,l,t,e,h)})},Jn=(s,t,e,i)=>{t.isDirectory()&&ps(Ee.resolve(s,t.name),e,i),us(Ee.resolve(s,t.name),e,i)},ps=(s,t,e)=>{let i;try{i=Ei.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return us(s,t,e);throw n}for(let r of i)Jn(s,r,t,e);return us(s,t,e)};import k from"node:fs";import jn from"node:fs/promises";import wi from"node:path";var we=class extends Error{path;code;syscall="chdir";constructor(t,e){super(`${e}: Cannot cd into '${t}'`),this.path=t,this.code=e}get name(){return"CwdError"}};var St=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(t,e){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=t,this.path=e}get name(){return"SymlinkError"}};var to=(s,t)=>{k.stat(s,(e,i)=>{(e||!i.isDirectory())&&(e=new we(s,e?.code||"ENOTDIR")),t(e)})},Sr=(s,t,e)=>{s=f(s);let i=t.umask??18,r=t.mode|448,n=(r&i)!==0,o=t.uid,a=t.gid,h=typeof o=="number"&&typeof a=="number"&&(o!==t.processUid||a!==t.processGid),l=t.preserve,c=t.unlink,d=f(t.cwd),S=(E,x)=>{E?e(E):x&&h?ms(x,o,a,xe=>S(xe)):n?k.chmod(s,r,e):e()};if(s===d)return to(s,S);if(l)return jn.mkdir(s,{mode:r,recursive:!0}).then(E=>S(null,E??void 0),S);let N=f(wi.relative(d,s)).split("/");Es(d,N,r,c,d,void 0,S)},Es=(s,t,e,i,r,n,o)=>{if(t.length===0)return o(null,n);let a=t.shift(),h=f(wi.resolve(s+"/"+a));k.mkdir(h,e,yr(h,t,e,i,r,n,o))},yr=(s,t,e,i,r,n,o)=>a=>{a?k.lstat(s,(h,l)=>{if(h)h.path=h.path&&f(h.path),o(h);else if(l.isDirectory())Es(s,t,e,i,r,n,o);else if(i)k.unlink(s,c=>{if(c)return o(c);k.mkdir(s,e,yr(s,t,e,i,r,n,o))});else{if(l.isSymbolicLink())return o(new St(s,s+"/"+t.join("/")));o(a)}}):(n=n||s,Es(s,t,e,i,r,n,o))},eo=s=>{let t=!1,e;try{t=k.statSync(s).isDirectory()}catch(i){e=i?.code}finally{if(!t)throw new we(s,e??"ENOTDIR")}},Rr=(s,t)=>{s=f(s);let e=t.umask??18,i=t.mode|448,r=(i&e)!==0,n=t.uid,o=t.gid,a=typeof n=="number"&&typeof o=="number"&&(n!==t.processUid||o!==t.processGid),h=t.preserve,l=t.unlink,c=f(t.cwd),d=E=>{E&&a&&ps(E,n,o),r&&k.chmodSync(s,i)};if(s===c)return eo(c),d();if(h)return d(k.mkdirSync(s,{mode:i,recursive:!0})??void 0);let T=f(wi.relative(c,s)).split("/"),N;for(let E=T.shift(),x=c;E&&(x+="/"+E);E=T.shift()){x=f(wi.resolve(x));try{k.mkdirSync(x,i),N=N||x}catch{let xe=k.lstatSync(x);if(xe.isDirectory())continue;if(l){k.unlinkSync(x),k.mkdirSync(x,i),N=N||x;continue}else if(xe.isSymbolicLink())return new St(x,x+"/"+T.join("/"))}}return d(N)};import{join as _r}from"node:path";var ws=Object.create(null),gr=1e4,Vt=new Set,br=s=>{Vt.has(s)?Vt.delete(s):ws[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),Vt.add(s);let t=ws[s],e=Vt.size-gr;if(e>gr/10){for(let i of Vt)if(Vt.delete(i),delete ws[i],--e<=0)break}return t};var io=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,so=io==="win32",ro=s=>s.split("/").slice(0,-1).reduce((e,i)=>{let r=e.at(-1);return r!==void 0&&(i=_r(r,i)),e.push(i||"/"),e},[]),Si=class{#t=new Map;#i=new Map;#s=new Set;reserve(t,e){t=so?["win32 parallelization disabled"]:t.map(r=>mt(_r(br(r))));let i=new Set(t.map(r=>ro(r)).reduce((r,n)=>r.concat(n)));this.#i.set(e,{dirs:i,paths:t});for(let r of t){let n=this.#t.get(r);n?n.push(e):this.#t.set(r,[e])}for(let r of i){let n=this.#t.get(r);if(!n)this.#t.set(r,[new Set([e])]);else{let o=n.at(-1);o instanceof Set?o.add(e):n.push(new Set([e]))}}return this.#r(e)}#n(t){let e=this.#i.get(t);if(!e)throw new Error("function does not have any path reservations");return{paths:e.paths.map(i=>this.#t.get(i)),dirs:[...e.dirs].map(i=>this.#t.get(i))}}check(t){let{paths:e,dirs:i}=this.#n(t);return e.every(r=>r&&r[0]===t)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(t))}#r(t){return this.#s.has(t)||!this.check(t)?!1:(this.#s.add(t),t(()=>this.#e(t)),!0)}#e(t){if(!this.#s.has(t))return!1;let e=this.#i.get(t);if(!e)throw new Error("invalid reservation");let{paths:i,dirs:r}=e,n=new Set;for(let o of i){let a=this.#t.get(o);if(!a||a?.[0]!==t)continue;let h=a[1];if(!h){this.#t.delete(o);continue}if(a.shift(),typeof h=="function")n.add(h);else for(let l of h)n.add(l)}for(let o of r){let a=this.#t.get(o),h=a?.[0];if(!(!a||!(h instanceof Set)))if(h.size===1&&a.length===1){this.#t.delete(o);continue}else if(h.size===1){a.shift();let l=a[0];typeof l=="function"&&n.add(l)}else h.delete(t)}return this.#s.delete(t),n.forEach(o=>this.#r(o)),!0}};var Or=()=>process.umask();var Tr=Symbol("onEntry"),gs=Symbol("checkFs"),xr=Symbol("checkFs2"),bs=Symbol("isReusable"),P=Symbol("makeFs"),_s=Symbol("file"),Os=Symbol("directory"),Ri=Symbol("link"),Lr=Symbol("symlink"),Nr=Symbol("hardlink"),ye=Symbol("ensureNoSymlink"),Dr=Symbol("unsupported"),Ar=Symbol("checkPath"),Ss=Symbol("stripAbsolutePath"),yt=Symbol("mkdir"),O=Symbol("onError"),yi=Symbol("pending"),Ir=Symbol("pend"),$t=Symbol("unpend"),ys=Symbol("ended"),Rs=Symbol("maybeClose"),Ts=Symbol("skip"),Re=Symbol("doChown"),ge=Symbol("uid"),be=Symbol("gid"),_e=Symbol("checkedCwd"),oo=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Oe=oo==="win32",ho=1024,ao=(s,t)=>{if(!Oe)return u.unlink(s,t);let e=s+".DELETE."+Cr(16).toString("hex");u.rename(s,e,i=>{if(i)return t(i);u.unlink(e,t)})},lo=s=>{if(!Oe)return u.unlinkSync(s);let t=s+".DELETE."+Cr(16).toString("hex");u.renameSync(s,t),u.unlinkSync(t)},Fr=(s,t,e)=>s!==void 0&&s===s>>>0?s:t!==void 0&&t===t>>>0?t:e,Xt=class extends st{[ys]=!1;[_e]=!1;[yi]=0;reservations=new Si;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(t={}){if(t.ondone=()=>{this[ys]=!0,this[Rs]()},super(t),this.transform=t.transform,this.chmod=!!t.chmod,typeof t.uid=="number"||typeof t.gid=="number"){if(typeof t.uid!="number"||typeof t.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(t.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=t.uid,this.gid=t.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=t.preserveOwner===void 0&&typeof t.uid!="number"?!!(process.getuid&&process.getuid()===0):!!t.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof t.maxDepth=="number"?t.maxDepth:ho,this.forceChown=t.forceChown===!0,this.win32=!!t.win32||Oe,this.newer=!!t.newer,this.keep=!!t.keep,this.noMtime=!!t.noMtime,this.preservePaths=!!t.preservePaths,this.unlink=!!t.unlink,this.cwd=f(R.resolve(t.cwd||process.cwd())),this.strip=Number(t.strip)||0,this.processUmask=this.chmod?typeof t.processUmask=="number"?t.processUmask:Or():0,this.umask=typeof t.umask=="number"?t.umask:this.processUmask,this.dmode=t.dmode||511&~this.umask,this.fmode=t.fmode||438&~this.umask,this.on("entry",e=>this[Tr](e))}warn(t,e,i={}){return(t==="TAR_BAD_ARCHIVE"||t==="TAR_ABORT")&&(i.recoverable=!1),super.warn(t,e,i)}[Rs](){this[ys]&&this[yi]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[Ss](t,e){let i=t[e],{type:r}=t;if(!i||this.preservePaths)return!0;let[n,o]=le(i),a=o.replaceAll(/\\/g,"/").split("/");if(a.includes("..")||Oe&&/^[a-z]:\.\.$/i.test(a[0]??"")){if(e==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${e} contains '..'`,{entry:t,[e]:i}),!1;let h=R.posix.dirname(t.path),l=R.posix.normalize(R.posix.join(h,a.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${e} escapes extraction directory`,{entry:t,[e]:i}),!1}return n&&(t[e]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${e}`,{entry:t,[e]:i})),!0}[Ar](t){let e=f(t.path),i=e.split("/");if(this.strip){if(i.length=this.strip)t.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),t.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:t,path:e,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[Ss](t,"path")||!this[Ss](t,"linkpath"))return!1;if(t.absolute=R.isAbsolute(t.path)?f(R.resolve(t.path)):f(R.resolve(this.cwd,t.path)),!this.preservePaths&&typeof t.absolute=="string"&&t.absolute.indexOf(this.cwd+"/")!==0&&t.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:t,path:f(t.path),resolvedPath:t.absolute,cwd:this.cwd}),!1;if(t.absolute===this.cwd&&t.type!=="Directory"&&t.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=R.win32.parse(String(t.absolute));t.absolute=r+Ji(String(t.absolute).slice(r.length));let{root:n}=R.win32.parse(t.path);t.path=n+Ji(t.path.slice(n.length))}return!0}[Tr](t){if(!this[Ar](t))return t.resume();switch(no.equal(typeof t.absolute,"string"),t.type){case"Directory":case"GNUDumpDir":t.mode&&(t.mode=t.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[gs](t);default:return this[Dr](t)}}[O](t,e){t.name==="CwdError"?this.emit("error",t):(this.warn("TAR_ENTRY_ERROR",t,{entry:e}),this[$t](),e.resume())}[yt](t,e,i){Sr(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e},i)}[Re](t){return this.forceChown||this.preserveOwner&&(typeof t.uid=="number"&&t.uid!==this.processUid||typeof t.gid=="number"&&t.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[ge](t){return Fr(this.uid,t.uid,this.processUid)}[be](t){return Fr(this.gid,t.gid,this.processGid)}[_s](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=new tt(String(t.absolute),{flags:ds(t.size),mode:i,autoClose:!1});r.on("error",h=>{r.fd&&u.close(r.fd,()=>{}),r.write=()=>!0,this[O](h,t),e()});let n=1,o=h=>{if(h){r.fd&&u.close(r.fd,()=>{}),this[O](h,t),e();return}--n===0&&r.fd!==void 0&&u.close(r.fd,l=>{l?this[O](l,t):this[$t](),e()})};r.on("finish",()=>{let h=String(t.absolute),l=r.fd;if(typeof l=="number"&&t.mtime&&!this.noMtime){n++;let c=t.atime||new Date,d=t.mtime;u.futimes(l,c,d,S=>S?u.utimes(h,c,d,T=>o(T&&S)):o())}if(typeof l=="number"&&this[Re](t)){n++;let c=this[ge](t),d=this[be](t);typeof c=="number"&&typeof d=="number"&&u.fchown(l,c,d,S=>S?u.chown(h,c,d,T=>o(T&&S)):o())}o()});let a=this.transform&&this.transform(t)||t;a!==t&&(a.on("error",h=>{this[O](h,t),e()}),t.pipe(a)),a.pipe(r)}[Os](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode;this[yt](String(t.absolute),i,r=>{if(r){this[O](r,t),e();return}let n=1,o=()=>{--n===0&&(e(),this[$t](),t.resume())};t.mtime&&!this.noMtime&&(n++,u.utimes(String(t.absolute),t.atime||new Date,t.mtime,o)),this[Re](t)&&(n++,u.chown(String(t.absolute),Number(this[ge](t)),Number(this[be](t)),o)),o()})}[Dr](t){t.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${t.type}`,{entry:t}),t.resume()}[Lr](t,e){let i=f(R.relative(this.cwd,R.resolve(R.dirname(String(t.absolute)),String(t.linkpath)))).split("/");this[ye](t,this.cwd,i,()=>this[Ri](t,String(t.linkpath),"symlink",e),r=>{this[O](r,t),e()})}[Nr](t,e){let i=f(R.resolve(this.cwd,String(t.linkpath))),r=f(String(t.linkpath)).split("/");this[ye](t,this.cwd,r,()=>this[Ri](t,i,"link",e),n=>{this[O](n,t),e()})}[ye](t,e,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let a=R.resolve(e,o);u.lstat(a,(h,l)=>{if(h)return r();if(l?.isSymbolicLink())return n(new St(a,R.resolve(a,i.join("/"))));this[ye](t,a,i,r,n)})}[Ir](){this[yi]++}[$t](){this[yi]--,this[Rs]()}[Ts](t){this[$t](),t.resume()}[bs](t,e){return t.type==="File"&&!this.unlink&&e.isFile()&&e.nlink<=1&&!Oe}[gs](t){this[Ir]();let e=[t.path];t.linkpath&&e.push(t.linkpath),this.reservations.reserve(e,i=>this[xr](t,i))}[xr](t,e){let i=a=>{e(a)},r=()=>{this[yt](this.cwd,this.dmode,a=>{if(a){this[O](a,t),i();return}this[_e]=!0,n()})},n=()=>{if(t.absolute!==this.cwd){let a=f(R.dirname(String(t.absolute)));if(a!==this.cwd)return this[yt](a,this.dmode,h=>{if(h){this[O](h,t),i();return}o()})}o()},o=()=>{u.lstat(String(t.absolute),(a,h)=>{if(h&&(this.keep||this.newer&&h.mtime>(t.mtime??h.mtime))){this[Ts](t),i();return}if(a||this[bs](t,h))return this[P](null,t,i);if(h.isDirectory()){if(t.type==="Directory"){let l=this.chmod&&t.mode&&(h.mode&4095)!==t.mode,c=d=>this[P](d??null,t,i);return l?u.chmod(String(t.absolute),Number(t.mode),c):c()}if(t.absolute!==this.cwd)return u.rmdir(String(t.absolute),l=>this[P](l??null,t,i))}if(t.absolute===this.cwd)return this[P](null,t,i);ao(String(t.absolute),l=>this[P](l??null,t,i))})};this[_e]?n():r()}[P](t,e,i){if(t){this[O](t,e),i();return}switch(e.type){case"File":case"OldFile":case"ContiguousFile":return this[_s](e,i);case"Link":return this[Nr](e,i);case"SymbolicLink":return this[Lr](e,i);case"Directory":case"GNUDumpDir":return this[Os](e,i)}}[Ri](t,e,i,r){u[i](e,String(t.absolute),n=>{n?this[O](n,t):(this[$t](),t.resume()),r()})}},Se=s=>{try{return[null,s()]}catch(t){return[t,null]}},Te=class extends Xt{sync=!0;[P](t,e){return super[P](t,e,()=>{})}[gs](t){if(!this[_e]){let n=this[yt](this.cwd,this.dmode);if(n)return this[O](n,t);this[_e]=!0}if(t.absolute!==this.cwd){let n=f(R.dirname(String(t.absolute)));if(n!==this.cwd){let o=this[yt](n,this.dmode);if(o)return this[O](o,t)}}let[e,i]=Se(()=>u.lstatSync(String(t.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(t.mtime??i.mtime)))return this[Ts](t);if(e||this[bs](t,i))return this[P](null,t);if(i.isDirectory()){if(t.type==="Directory"){let o=this.chmod&&t.mode&&(i.mode&4095)!==t.mode,[a]=o?Se(()=>{u.chmodSync(String(t.absolute),Number(t.mode))}):[];return this[P](a,t)}let[n]=Se(()=>u.rmdirSync(String(t.absolute)));this[P](n,t)}let[r]=t.absolute===this.cwd?[]:Se(()=>lo(String(t.absolute)));this[P](r,t)}[_s](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=a=>{let h;try{u.closeSync(n)}catch(l){h=l}(a||h)&&this[O](a||h,t),e()},n;try{n=u.openSync(String(t.absolute),ds(t.size),i)}catch(a){return r(a)}let o=this.transform&&this.transform(t)||t;o!==t&&(o.on("error",a=>this[O](a,t)),t.pipe(o)),o.on("data",a=>{try{u.writeSync(n,a,0,a.length)}catch(h){r(h)}}),o.on("end",()=>{let a=null;if(t.mtime&&!this.noMtime){let h=t.atime||new Date,l=t.mtime;try{u.futimesSync(n,h,l)}catch(c){try{u.utimesSync(String(t.absolute),h,l)}catch{a=c}}}if(this[Re](t)){let h=this[ge](t),l=this[be](t);try{u.fchownSync(n,Number(h),Number(l))}catch(c){try{u.chownSync(String(t.absolute),Number(h),Number(l))}catch{a=a||c}}}r(a)})}[Os](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode,r=this[yt](String(t.absolute),i);if(r){this[O](r,t),e();return}if(t.mtime&&!this.noMtime)try{u.utimesSync(String(t.absolute),t.atime||new Date,t.mtime)}catch{}if(this[Re](t))try{u.chownSync(String(t.absolute),Number(this[ge](t)),Number(this[be](t)))}catch{}e(),t.resume()}[yt](t,e){try{return Rr(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e})}catch(i){return i}}[ye](t,e,i,r,n){if(this.preservePaths||i.length===0)return r();let o=e;for(let a of i){o=R.resolve(o,a);let[h,l]=Se(()=>u.lstatSync(o));if(h)return r();if(l.isSymbolicLink())return n(new St(o,R.resolve(e,i.join("/"))))}r()}[Ri](t,e,i,r){let n=`${i}Sync`;try{u[n](e,String(t.absolute)),r(),t.resume()}catch(o){return this[O](o,t)}}};var co=s=>{let t=new Te(s),e=s.file,i=kr.statSync(e),r=s.maxReadSize||16*1024*1024;new Me(e,{readSize:r,size:i.size}).pipe(t)},fo=(s,t)=>{let e=new Xt(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,a)=>{e.on("error",a),e.on("close",o),kr.stat(r,(h,l)=>{if(h)a(h);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",a),c.pipe(e)}})})},uo=K(co,fo,s=>new Te(s),s=>new Xt(s),(s,t)=>{t?.length&&Xi(s,t)});import v from"node:fs";import vr from"node:path";var mo=(s,t)=>{let e=new kt(s),i=!0,r,n;try{try{r=v.openSync(s.file,"r+")}catch(h){if(h?.code==="ENOENT")r=v.openSync(s.file,"w+");else throw h}let o=v.fstatSync(r),a=Buffer.alloc(512);t:for(n=0;no.size)break;n+=l,s.mtimeCache&&h.mtime&&s.mtimeCache.set(String(h.path),h.mtime)}i=!1,po(s,e,n,r,t)}finally{if(i)try{v.closeSync(r)}catch{}}},po=(s,t,e,i,r)=>{let n=new Wt(s.file,{fd:i,start:e});t.pipe(n),wo(t,r)},Eo=(s,t)=>{t=Array.from(t);let e=new wt(s),i=(n,o,a)=>{let h=(T,N)=>{T?v.close(n,E=>a(T)):a(null,N)},l=0;if(o===0)return h(null,0);let c=0,d=Buffer.alloc(512),S=(T,N)=>{if(T||N===void 0)return h(T);if(c+=N,c<512&&N)return v.read(n,d,c,d.length-c,l+c,S);if(l===0&&d[0]===31&&d[1]===139)return h(new Error("cannot append to compressed archives"));if(c<512)return h(null,l);let E=new C(d);if(!E.cksumValid)return h(null,l);let x=512*Math.ceil((E.size??0)/512);if(l+x+512>o||(l+=x+512,l>=o))return h(null,l);s.mtimeCache&&E.mtime&&s.mtimeCache.set(String(E.path),E.mtime),c=0,v.read(n,d,0,512,l,S)};v.read(n,d,0,512,l,S)};return new Promise((n,o)=>{e.on("error",o);let a="r+",h=(l,c)=>{if(l&&l.code==="ENOENT"&&a==="r+")return a="w+",v.open(s.file,a,h);if(l||!c)return o(l);v.fstat(c,(d,S)=>{if(d)return v.close(c,()=>o(d));i(c,S.size,(T,N)=>{if(T)return o(T);let E=new tt(s.file,{fd:c,start:N});e.pipe(E),E.on("error",o),E.on("close",n),So(e,t)})})};v.open(s.file,a,h)})},wo=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?Ft({file:vr.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},So=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await Ft({file:vr.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e);s.end()},vt=K(mo,Eo,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,t)=>{if(!vs(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!t?.length)throw new TypeError("no paths specified to add/replace")});var yo=K(vt.syncFile,vt.asyncFile,vt.syncNoFile,vt.asyncNoFile,(s,t=[])=>{vt.validate?.(s,t),Ro(s)}),Ro=s=>{let t=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=t?(e,i)=>t(e,i)&&!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0)):(e,i)=>!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0))};export{C as Header,wt as Pack,mi as PackJob,kt as PackSync,st as Parser,ct as Pax,Ve as ReadEntry,Xt as Unpack,Te as UnpackSync,fe as WriteEntry,ri as WriteEntrySync,ni as WriteEntryTar,Kn as c,Kn as create,uo as extract,Xi as filesFilter,Ft as list,vt as r,vt as replace,Ft as t,Ui as types,yo as u,yo as update,uo as x}; +var zr=Object.defineProperty;var Ur=(s,t)=>{for(var e in t)zr(s,e,{get:t[e],enumerable:!0})};import Qr from"events";import I from"fs";import{EventEmitter as Di}from"node:events";import Cs from"node:stream";import{StringDecoder as Hr}from"node:string_decoder";var Ds=typeof process=="object"&&process?process:{stdout:null,stderr:null},Wr=s=>!!s&&typeof s=="object"&&(s instanceof A||s instanceof Cs||Gr(s)||Zr(s)),Gr=s=>!!s&&typeof s=="object"&&s instanceof Di&&typeof s.pipe=="function"&&s.pipe!==Cs.Writable.prototype.pipe,Zr=s=>!!s&&typeof s=="object"&&s instanceof Di&&typeof s.write=="function"&&typeof s.end=="function",Q=Symbol("EOF"),J=Symbol("maybeEmitEnd"),nt=Symbol("emittedEnd"),De=Symbol("emittingEnd"),qt=Symbol("emittedError"),Ne=Symbol("closed"),Ns=Symbol("read"),Ae=Symbol("flush"),As=Symbol("flushChunk"),z=Symbol("encoding"),Mt=Symbol("decoder"),g=Symbol("flowing"),Qt=Symbol("paused"),Bt=Symbol("resume"),b=Symbol("buffer"),N=Symbol("pipes"),_=Symbol("bufferLength"),bi=Symbol("bufferPush"),Ie=Symbol("bufferShift"),L=Symbol("objectMode"),w=Symbol("destroyed"),_i=Symbol("error"),Oi=Symbol("emitData"),Is=Symbol("emitEnd"),Ti=Symbol("emitEnd2"),Z=Symbol("async"),xi=Symbol("abort"),Ce=Symbol("aborted"),Jt=Symbol("signal"),Rt=Symbol("dataListeners"),C=Symbol("discarded"),jt=s=>Promise.resolve().then(s),Yr=s=>s(),Kr=s=>s==="end"||s==="finish"||s==="prefinish",Vr=s=>s instanceof ArrayBuffer||!!s&&typeof s=="object"&&s.constructor&&s.constructor.name==="ArrayBuffer"&&s.byteLength>=0,$r=s=>!Buffer.isBuffer(s)&&ArrayBuffer.isView(s),Fe=class{src;dest;opts;ondrain;constructor(t,e,i){this.src=t,this.dest=e,this.opts=i,this.ondrain=()=>t[Bt](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(t){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},Li=class extends Fe{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(t,e,i){super(t,e,i),this.proxyErrors=r=>this.dest.emit("error",r),t.on("error",this.proxyErrors)}},Xr=s=>!!s.objectMode,qr=s=>!s.objectMode&&!!s.encoding&&s.encoding!=="buffer",A=class extends Di{[g]=!1;[Qt]=!1;[N]=[];[b]=[];[L];[z];[Z];[Mt];[Q]=!1;[nt]=!1;[De]=!1;[Ne]=!1;[qt]=null;[_]=0;[w]=!1;[Jt];[Ce]=!1;[Rt]=0;[C]=!1;writable=!0;readable=!0;constructor(...t){let e=t[0]||{};if(super(),e.objectMode&&typeof e.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Xr(e)?(this[L]=!0,this[z]=null):qr(e)?(this[z]=e.encoding,this[L]=!1):(this[L]=!1,this[z]=null),this[Z]=!!e.async,this[Mt]=this[z]?new Hr(this[z]):null,e&&e.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[b]}),e&&e.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[N]});let{signal:i}=e;i&&(this[Jt]=i,i.aborted?this[xi]():i.addEventListener("abort",()=>this[xi]()))}get bufferLength(){return this[_]}get encoding(){return this[z]}set encoding(t){throw new Error("Encoding must be set at instantiation time")}setEncoding(t){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[L]}set objectMode(t){throw new Error("objectMode must be set at instantiation time")}get async(){return this[Z]}set async(t){this[Z]=this[Z]||!!t}[xi](){this[Ce]=!0,this.emit("abort",this[Jt]?.reason),this.destroy(this[Jt]?.reason)}get aborted(){return this[Ce]}set aborted(t){}write(t,e,i){if(this[Ce])return!1;if(this[Q])throw new Error("write after end");if(this[w])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof e=="function"&&(i=e,e="utf8"),e||(e="utf8");let r=this[Z]?jt:Yr;if(!this[L]&&!Buffer.isBuffer(t)){if($r(t))t=Buffer.from(t.buffer,t.byteOffset,t.byteLength);else if(Vr(t))t=Buffer.from(t);else if(typeof t!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[L]?(this[g]&&this[_]!==0&&this[Ae](!0),this[g]?this.emit("data",t):this[bi](t),this[_]!==0&&this.emit("readable"),i&&r(i),this[g]):t.length?(typeof t=="string"&&!(e===this[z]&&!this[Mt]?.lastNeed)&&(t=Buffer.from(t,e)),Buffer.isBuffer(t)&&this[z]&&(t=this[Mt].write(t)),this[g]&&this[_]!==0&&this[Ae](!0),this[g]?this.emit("data",t):this[bi](t),this[_]!==0&&this.emit("readable"),i&&r(i),this[g]):(this[_]!==0&&this.emit("readable"),i&&r(i),this[g])}read(t){if(this[w])return null;if(this[C]=!1,this[_]===0||t===0||t&&t>this[_])return this[J](),null;this[L]&&(t=null),this[b].length>1&&!this[L]&&(this[b]=[this[z]?this[b].join(""):Buffer.concat(this[b],this[_])]);let e=this[Ns](t||null,this[b][0]);return this[J](),e}[Ns](t,e){if(this[L])this[Ie]();else{let i=e;t===i.length||t===null?this[Ie]():typeof i=="string"?(this[b][0]=i.slice(t),e=i.slice(0,t),this[_]-=t):(this[b][0]=i.subarray(t),e=i.subarray(0,t),this[_]-=t)}return this.emit("data",e),!this[b].length&&!this[Q]&&this.emit("drain"),e}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e="utf8"),t!==void 0&&this.write(t,e),i&&this.once("end",i),this[Q]=!0,this.writable=!1,(this[g]||!this[Qt])&&this[J](),this}[Bt](){this[w]||(!this[Rt]&&!this[N].length&&(this[C]=!0),this[Qt]=!1,this[g]=!0,this.emit("resume"),this[b].length?this[Ae]():this[Q]?this[J]():this.emit("drain"))}resume(){return this[Bt]()}pause(){this[g]=!1,this[Qt]=!0,this[C]=!1}get destroyed(){return this[w]}get flowing(){return this[g]}get paused(){return this[Qt]}[bi](t){this[L]?this[_]+=1:this[_]+=t.length,this[b].push(t)}[Ie](){return this[L]?this[_]-=1:this[_]-=this[b][0].length,this[b].shift()}[Ae](t=!1){do;while(this[As](this[Ie]())&&this[b].length);!t&&!this[b].length&&!this[Q]&&this.emit("drain")}[As](t){return this.emit("data",t),this[g]}pipe(t,e){if(this[w])return t;this[C]=!1;let i=this[nt];return e=e||{},t===Ds.stdout||t===Ds.stderr?e.end=!1:e.end=e.end!==!1,e.proxyErrors=!!e.proxyErrors,i?e.end&&t.end():(this[N].push(e.proxyErrors?new Li(this,t,e):new Fe(this,t,e)),this[Z]?jt(()=>this[Bt]()):this[Bt]()),t}unpipe(t){let e=this[N].find(i=>i.dest===t);e&&(this[N].length===1?(this[g]&&this[Rt]===0&&(this[g]=!1),this[N]=[]):this[N].splice(this[N].indexOf(e),1),e.unpipe())}addListener(t,e){return this.on(t,e)}on(t,e){let i=super.on(t,e);if(t==="data")this[C]=!1,this[Rt]++,!this[N].length&&!this[g]&&this[Bt]();else if(t==="readable"&&this[_]!==0)super.emit("readable");else if(Kr(t)&&this[nt])super.emit(t),this.removeAllListeners(t);else if(t==="error"&&this[qt]){let r=e;this[Z]?jt(()=>r.call(this,this[qt])):r.call(this,this[qt])}return i}removeListener(t,e){return this.off(t,e)}off(t,e){let i=super.off(t,e);return t==="data"&&(this[Rt]=this.listeners("data").length,this[Rt]===0&&!this[C]&&!this[N].length&&(this[g]=!1)),i}removeAllListeners(t){let e=super.removeAllListeners(t);return(t==="data"||t===void 0)&&(this[Rt]=0,!this[C]&&!this[N].length&&(this[g]=!1)),e}get emittedEnd(){return this[nt]}[J](){!this[De]&&!this[nt]&&!this[w]&&this[b].length===0&&this[Q]&&(this[De]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[Ne]&&this.emit("close"),this[De]=!1)}emit(t,...e){let i=e[0];if(t!=="error"&&t!=="close"&&t!==w&&this[w])return!1;if(t==="data")return!this[L]&&!i?!1:this[Z]?(jt(()=>this[Oi](i)),!0):this[Oi](i);if(t==="end")return this[Is]();if(t==="close"){if(this[Ne]=!0,!this[nt]&&!this[w])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(t==="error"){this[qt]=i,super.emit(_i,i);let n=!this[Jt]||this.listeners("error").length?super.emit("error",i):!1;return this[J](),n}else if(t==="resume"){let n=super.emit("resume");return this[J](),n}else if(t==="finish"||t==="prefinish"){let n=super.emit(t);return this.removeAllListeners(t),n}let r=super.emit(t,...e);return this[J](),r}[Oi](t){for(let i of this[N])i.dest.write(t)===!1&&this.pause();let e=this[C]?!1:super.emit("data",t);return this[J](),e}[Is](){return this[nt]?!1:(this[nt]=!0,this.readable=!1,this[Z]?(jt(()=>this[Ti]()),!0):this[Ti]())}[Ti](){if(this[Mt]){let e=this[Mt].end();if(e){for(let i of this[N])i.dest.write(e);this[C]||super.emit("data",e)}}for(let e of this[N])e.end();let t=super.emit("end");return this.removeAllListeners("end"),t}async collect(){let t=Object.assign([],{dataLength:0});this[L]||(t.dataLength=0);let e=this.promise();return this.on("data",i=>{t.push(i),this[L]||(t.dataLength+=i.length)}),await e,t}async concat(){if(this[L])throw new Error("cannot concat in objectMode");let t=await this.collect();return this[z]?t.join(""):Buffer.concat(t,t.dataLength)}async promise(){return new Promise((t,e)=>{this.on(w,()=>e(new Error("stream destroyed"))),this.on("error",i=>e(i)),this.on("end",()=>t())})}[Symbol.asyncIterator](){this[C]=!1;let t=!1,e=async()=>(this.pause(),t=!0,{value:void 0,done:!0});return{next:()=>{if(t)return e();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[Q])return e();let n,o,h=d=>{this.off("data",a),this.off("end",l),this.off(w,c),e(),o(d)},a=d=>{this.off("error",h),this.off("end",l),this.off(w,c),this.pause(),n({value:d,done:!!this[Q]})},l=()=>{this.off("error",h),this.off("data",a),this.off(w,c),e(),n({done:!0,value:void 0})},c=()=>h(new Error("stream destroyed"));return new Promise((d,S)=>{o=S,n=d,this.once(w,c),this.once("error",h),this.once("end",l),this.once("data",a)})},throw:e,return:e,[Symbol.asyncIterator](){return this},[Symbol.asyncDispose]:async()=>{}}}[Symbol.iterator](){this[C]=!1;let t=!1,e=()=>(this.pause(),this.off(_i,e),this.off(w,e),this.off("end",e),t=!0,{done:!0,value:void 0}),i=()=>{if(t)return e();let r=this.read();return r===null?e():{done:!1,value:r}};return this.once("end",e),this.once(_i,e),this.once(w,e),{next:i,throw:e,return:e,[Symbol.iterator](){return this},[Symbol.dispose]:()=>{}}}destroy(t){if(this[w])return t?this.emit("error",t):this.emit(w),this;this[w]=!0,this[C]=!0,this[b].length=0,this[_]=0;let e=this;return typeof e.close=="function"&&!this[Ne]&&e.close(),t?this.emit("error",t):this.emit(w),this}static get isStream(){return Wr}};var Jr=I.writev,ht=Symbol("_autoClose"),H=Symbol("_close"),te=Symbol("_ended"),u=Symbol("_fd"),Ni=Symbol("_finished"),tt=Symbol("_flags"),Ai=Symbol("_flush"),ki=Symbol("_handleChunk"),vi=Symbol("_makeBuf"),ie=Symbol("_mode"),ke=Symbol("_needDrain"),Ut=Symbol("_onerror"),Ht=Symbol("_onopen"),Ii=Symbol("_onread"),Pt=Symbol("_onwrite"),at=Symbol("_open"),U=Symbol("_path"),ot=Symbol("_pos"),Y=Symbol("_queue"),zt=Symbol("_read"),Ci=Symbol("_readSize"),j=Symbol("_reading"),ee=Symbol("_remain"),Fi=Symbol("_size"),ve=Symbol("_write"),gt=Symbol("_writing"),Me=Symbol("_defaultFlag"),bt=Symbol("_errored"),_t=class extends A{[bt]=!1;[u];[U];[Ci];[j]=!1;[Fi];[ee];[ht];constructor(t,e){if(e=e||{},super(e),this.readable=!0,this.writable=!1,typeof t!="string")throw new TypeError("path must be a string");this[bt]=!1,this[u]=typeof e.fd=="number"?e.fd:void 0,this[U]=t,this[Ci]=e.readSize||16*1024*1024,this[j]=!1,this[Fi]=typeof e.size=="number"?e.size:1/0,this[ee]=this[Fi],this[ht]=typeof e.autoClose=="boolean"?e.autoClose:!0,typeof this[u]=="number"?this[zt]():this[at]()}get fd(){return this[u]}get path(){return this[U]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[at](){I.open(this[U],"r",(t,e)=>this[Ht](t,e))}[Ht](t,e){t?this[Ut](t):(this[u]=e,this.emit("open",e),this[zt]())}[vi](){return Buffer.allocUnsafe(Math.min(this[Ci],this[ee]))}[zt](){if(!this[j]){this[j]=!0;let t=this[vi]();if(t.length===0)return process.nextTick(()=>this[Ii](null,0,t));I.read(this[u],t,0,t.length,null,(e,i,r)=>this[Ii](e,i,r))}}[Ii](t,e,i){this[j]=!1,t?this[Ut](t):this[ki](e,i)&&this[zt]()}[H](){if(this[ht]&&typeof this[u]=="number"){let t=this[u];this[u]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}[Ut](t){this[j]=!0,this[H](),this.emit("error",t)}[ki](t,e){let i=!1;return this[ee]-=t,t>0&&(i=super.write(tthis[Ht](t,e))}[Ht](t,e){this[Me]&&this[tt]==="r+"&&t&&t.code==="ENOENT"?(this[tt]="w",this[at]()):t?this[Ut](t):(this[u]=e,this.emit("open",e),this[gt]||this[Ai]())}end(t,e){return t&&this.write(t,e),this[te]=!0,!this[gt]&&!this[Y].length&&typeof this[u]=="number"&&this[Pt](null,0),this}write(t,e){return typeof t=="string"&&(t=Buffer.from(t,e)),this[te]?(this.emit("error",new Error("write() after end()")),!1):this[u]===void 0||this[gt]||this[Y].length?(this[Y].push(t),this[ke]=!0,!1):(this[gt]=!0,this[ve](t),!0)}[ve](t){I.write(this[u],t,0,t.length,this[ot],(e,i)=>this[Pt](e,i))}[Pt](t,e){t?this[Ut](t):(this[ot]!==void 0&&typeof e=="number"&&(this[ot]+=e),this[Y].length?this[Ai]():(this[gt]=!1,this[te]&&!this[Ni]?(this[Ni]=!0,this[H](),this.emit("finish")):this[ke]&&(this[ke]=!1,this.emit("drain"))))}[Ai](){if(this[Y].length===0)this[te]&&this[Pt](null,0);else if(this[Y].length===1)this[ve](this[Y].pop());else{let t=this[Y];this[Y]=[],Jr(this[u],t,this[ot],(e,i)=>this[Pt](e,i))}}[H](){if(this[ht]&&typeof this[u]=="number"){let t=this[u];this[u]=void 0,I.close(t,e=>e?this.emit("error",e):this.emit("close"))}}},Wt=class extends et{[at](){let t;if(this[Me]&&this[tt]==="r+")try{t=I.openSync(this[U],this[tt],this[ie])}catch(e){if(e?.code==="ENOENT")return this[tt]="w",this[at]();throw e}else t=I.openSync(this[U],this[tt],this[ie]);this[Ht](null,t)}[H](){if(this[ht]&&typeof this[u]=="number"){let t=this[u];this[u]=void 0,I.closeSync(t),this.emit("close")}}[ve](t){let e=!0;try{this[Pt](null,I.writeSync(this[u],t,0,t.length,this[ot])),e=!1}finally{if(e)try{this[H]()}catch{}}}};import cr from"node:path";import Kt from"node:fs";import{dirname as Fn,parse as kn}from"path";var jr=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),Fs=s=>!!s.sync&&!!s.file,ks=s=>!s.sync&&!!s.file,vs=s=>!!s.sync&&!s.file,Ms=s=>!s.sync&&!s.file;var Bs=s=>!!s.file;var tn=s=>{let t=jr.get(s);return t||s},se=(s={})=>{if(!s)return{};let t={};for(let[e,i]of Object.entries(s)){let r=tn(e);t[r]=i}return t.chmod===void 0&&t.noChmod===!1&&(t.chmod=!0),delete t.noChmod,t};var K=(s,t,e,i,r)=>Object.assign((n=[],o,h)=>{Array.isArray(n)&&(o=n,n={}),typeof o=="function"&&(h=o,o=void 0),o=o?Array.from(o):[];let a=se(n);if(r?.(a,o),Fs(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return s(a,o)}else if(ks(a)){let l=t(a,o);return h?l.then(()=>h(),h):l}else if(vs(a)){if(typeof h=="function")throw new TypeError("callback not supported for sync tar functions");return e(a,o)}else if(Ms(a)){if(typeof h=="function")throw new TypeError("callback only supported with file option");return i(a,o)}throw new Error("impossible options??")},{syncFile:s,asyncFile:t,syncNoFile:e,asyncNoFile:i,validate:r});import{EventEmitter as Dn}from"events";import zi from"assert";import{Buffer as Ot}from"buffer";import*as Ps from"zlib";import en from"zlib";var sn=en.constants||{ZLIB_VERNUM:4736},M=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},sn));var rn=Ot.concat,zs=Object.getOwnPropertyDescriptor(Ot,"concat"),nn=s=>s,Bi=zs?.writable===!0||zs?.set!==void 0?s=>{Ot.concat=s?nn:rn}:s=>{},Tt=Symbol("_superWrite"),Gt=class extends Error{code;errno;constructor(t,e){super("zlib: "+t.message,{cause:t}),this.code=t.code,this.errno=t.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+t.message,Error.captureStackTrace(this,e??this.constructor)}get name(){return"ZlibError"}},Pi=Symbol("flushFlag"),re=class extends A{#t=!1;#i=!1;#s;#n;#r;#e;#o;get sawError(){return this.#t}get handle(){return this.#e}get flushFlag(){return this.#s}constructor(t,e){if(!t||typeof t!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(t),this.#s=t.flush??0,this.#n=t.finishFlush??0,this.#r=t.fullFlushFlag??0,typeof Ps[e]!="function")throw new TypeError("Compression method not supported: "+e);try{this.#e=new Ps[e](t)}catch(i){throw new Gt(i,this.constructor)}this.#o=i=>{this.#t||(this.#t=!0,this.close(),this.emit("error",i))},this.#e?.on("error",i=>this.#o(new Gt(i))),this.once("end",()=>this.close)}close(){this.#e&&(this.#e.close(),this.#e=void 0,this.emit("close"))}reset(){if(!this.#t)return zi(this.#e,"zlib binding closed"),this.#e.reset?.()}flush(t){this.ended||(typeof t!="number"&&(t=this.#r),this.write(Object.assign(Ot.alloc(0),{[Pi]:t})))}end(t,e,i){return typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&(e?this.write(t,e):this.write(t)),this.flush(this.#n),this.#i=!0,super.end(i)}get ended(){return this.#i}[Tt](t){return super.write(t)}write(t,e,i){if(typeof e=="function"&&(i=e,e="utf8"),typeof t=="string"&&(t=Ot.from(t,e)),this.#t)return;zi(this.#e,"zlib binding closed");let r=this.#e._handle,n=r.close;r.close=()=>{};let o=this.#e.close;this.#e.close=()=>{},Bi(!0);let h;try{let l=typeof t[Pi]=="number"?t[Pi]:this.#s;h=this.#e._processChunk(t,l),Bi(!1)}catch(l){Bi(!1),this.#o(new Gt(l,this.write))}finally{this.#e&&(this.#e._handle=r,r.close=n,this.#e.close=o,this.#e.removeAllListeners("error"))}this.#e&&this.#e.on("error",l=>this.#o(new Gt(l,this.write)));let a;if(h)if(Array.isArray(h)&&h.length>0){let l=h[0];a=this[Tt](Ot.from(l));for(let c=1;c{typeof r=="function"&&(n=r,r=this.flushFlag),this.flush(r),n?.()};try{this.handle.params(t,e)}finally{this.handle.flush=i}this.handle&&(this.#t=t,this.#i=e)}}}};var ze=class extends Pe{#t;constructor(t){super(t,"Gzip"),this.#t=t&&!!t.portable}[Tt](t){return this.#t?(this.#t=!1,t[9]=255,super[Tt](t)):super[Tt](t)}};var Ue=class extends Pe{constructor(t){super(t,"Unzip")}},He=class extends re{constructor(t,e){t=t||{},t.flush=t.flush||M.BROTLI_OPERATION_PROCESS,t.finishFlush=t.finishFlush||M.BROTLI_OPERATION_FINISH,t.fullFlushFlag=M.BROTLI_OPERATION_FLUSH,super(t,e)}},We=class extends He{constructor(t){super(t,"BrotliCompress")}},Ge=class extends He{constructor(t){super(t,"BrotliDecompress")}},Ze=class extends re{constructor(t,e){t=t||{},t.flush=t.flush||M.ZSTD_e_continue,t.finishFlush=t.finishFlush||M.ZSTD_e_end,t.fullFlushFlag=M.ZSTD_e_flush,super(t,e)}},Ye=class extends Ze{constructor(t){super(t,"ZstdCompress")}},Ke=class extends Ze{constructor(t){super(t,"ZstdDecompress")}};import{posix as Zt}from"node:path";var Us=(s,t)=>{if(Number.isSafeInteger(s))s<0?an(s,t):hn(s,t);else throw Error("cannot encode number outside of javascript safe integer range");return t},hn=(s,t)=>{t[0]=128;for(var e=t.length;e>1;e--)t[e-1]=s&255,s=Math.floor(s/256)},an=(s,t)=>{t[0]=255;var e=!1;s=s*-1;for(var i=t.length;i>1;i--){var r=s&255;s=Math.floor(s/256),e?t[i-1]=Ws(r):r===0?t[i-1]=0:(e=!0,t[i-1]=Gs(r))}},Hs=s=>{let t=s[0],e=t===128?cn(s.subarray(1,s.length)):t===255?ln(s):null;if(e===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(e))throw Error("parsed number outside of javascript safe integer range");return e},ln=s=>{for(var t=s.length,e=0,i=!1,r=t-1;r>-1;r--){var n=Number(s[r]),o;i?o=Ws(n):n===0?o=n:(i=!0,o=Gs(n)),o!==0&&(e-=o*Math.pow(256,t-r-1))}return e},cn=s=>{for(var t=s.length,e=0,i=t-1;i>-1;i--){var r=Number(s[i]);r!==0&&(e+=r*Math.pow(256,t-i-1))}return e},Ws=s=>(255^s)&255,Gs=s=>(255^s)+1&255;var Hi={};Ur(Hi,{code:()=>Ve,isCode:()=>ne,isName:()=>dn,name:()=>oe,normalFsTypes:()=>Ui});var ne=s=>oe.has(s),dn=s=>Ve.has(s),Ui=new Set(["0","","1","2","3","4","5","6","7","D"]),oe=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]),Ve=new Map(Array.from(oe).map(s=>[s[1],s[0]]));var mn=s=>s===void 0||s<0?void 0:s,F=class{cksumValid=!1;needPax=!1;nullBlock=!1;block;path;mode;uid;gid;size;cksum;#t="Unsupported";linkpath;uname;gname;devmaj=0;devmin=0;atime;ctime;mtime;charset;comment;constructor(t,e=0,i,r){Buffer.isBuffer(t)?this.decode(t,e||0,i,r):t&&this.#i(t)}decode(t,e,i,r){if(e||(e=0),!t||!(t.length>=e+512))throw new Error("need 512 bytes for header");let n=xt(t,e+156,1),o=Ui.has(n),h=o?i:void 0,a=o?r:void 0;if(this.path=h?.path??xt(t,e,100),this.mode=h?.mode??a?.mode??lt(t,e+100,8),this.uid=h?.uid??a?.uid??lt(t,e+108,8),this.gid=h?.gid??a?.gid??lt(t,e+116,8),this.size=mn(h?.size??a?.size??lt(t,e+124,12)),this.mtime=h?.mtime??a?.mtime??Wi(t,e+136,12),this.cksum=lt(t,e+148,12),a&&this.#i(a,!0),h&&this.#i(h),ne(n)&&(this.#t=n||"0"),this.#t==="0"&&this.path.slice(-1)==="/"&&(this.#t="5"),this.#t==="5"&&(this.size=0),this.linkpath=xt(t,e+157,100),t.subarray(e+257,e+265).toString()==="ustar\x0000")if(this.uname=h?.uname??a?.uname??xt(t,e+265,32),this.gname=h?.gname??a?.gname??xt(t,e+297,32),this.devmaj=h?.devmaj??a?.devmaj??lt(t,e+329,8)??0,this.devmin=h?.devmin??a?.devmin??lt(t,e+337,8)??0,t[e+475]!==0){let c=xt(t,e+345,155);this.path=c+"/"+this.path}else{let c=xt(t,e+345,130);c&&(this.path=c+"/"+this.path),this.atime=i?.atime??r?.atime??Wi(t,e+476,12),this.ctime=i?.ctime??r?.ctime??Wi(t,e+488,12)}let l=256;for(let c=e;c!(r==null||i==="size"&&Number(r)<0||i==="path"&&e||i==="linkpath"&&e||i==="global"))))}encode(t,e=0){if(t||(t=this.block=Buffer.alloc(512)),this.#t==="Unsupported"&&(this.#t="0"),!(t.length>=e+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,r=un(this.path||"",i),n=r[0],o=r[1];this.needPax=!!r[2],this.needPax=Lt(t,e,100,n)||this.needPax,this.needPax=ct(t,e+100,8,this.mode)||this.needPax,this.needPax=ct(t,e+108,8,this.uid)||this.needPax,this.needPax=ct(t,e+116,8,this.gid)||this.needPax,this.needPax=ct(t,e+124,12,this.size)||this.needPax,this.needPax=Gi(t,e+136,12,this.mtime)||this.needPax,t[e+156]=Number(this.#t.codePointAt(0)),this.needPax=Lt(t,e+157,100,this.linkpath)||this.needPax,t.write("ustar\x0000",e+257,8),this.needPax=Lt(t,e+265,32,this.uname)||this.needPax,this.needPax=Lt(t,e+297,32,this.gname)||this.needPax,this.needPax=ct(t,e+329,8,this.devmaj)||this.needPax,this.needPax=ct(t,e+337,8,this.devmin)||this.needPax,this.needPax=Lt(t,e+345,i,o)||this.needPax,t[e+475]!==0?this.needPax=Lt(t,e+345,155,o)||this.needPax:(this.needPax=Lt(t,e+345,130,o)||this.needPax,this.needPax=Gi(t,e+476,12,this.atime)||this.needPax,this.needPax=Gi(t,e+488,12,this.ctime)||this.needPax);let h=256;for(let a=e;a{let i=s,r="",n,o=Zt.parse(s).root||".";if(Buffer.byteLength(i)<100)n=[i,r,!1];else{r=Zt.dirname(i),i=Zt.basename(i);do Buffer.byteLength(i)<=100&&Buffer.byteLength(r)<=t?n=[i,r,!1]:Buffer.byteLength(i)>100&&Buffer.byteLength(r)<=t?n=[i.slice(0,99),r,!0]:(i=Zt.join(Zt.basename(r),i),r=Zt.dirname(r));while(r!==o&&n===void 0);n||(n=[s.slice(0,99),"",!0])}return n},xt=(s,t,e)=>s.subarray(t,t+e).toString("utf8").replace(/\0.*/,""),Wi=(s,t,e)=>pn(lt(s,t,e)),pn=s=>s===void 0?void 0:new Date(s*1e3),lt=(s,t,e)=>Number(s[t])&128?Hs(s.subarray(t,t+e)):wn(s,t,e),En=s=>isNaN(s)?void 0:s,wn=(s,t,e)=>En(parseInt(s.subarray(t,t+e).toString("utf8").replace(/\0.*$/,"").trim(),8)),Sn={12:8589934591,8:2097151},ct=(s,t,e,i)=>i===void 0?!1:i>Sn[e]||i<0?(Us(i,s.subarray(t,t+e)),!0):(yn(s,t,e,i),!1),yn=(s,t,e,i)=>s.write(Rn(i,e),t,e,"ascii"),Rn=(s,t)=>gn(Math.floor(s).toString(8),t),gn=(s,t)=>(s.length===t-1?s:new Array(t-s.length-1).join("0")+s+" ")+"\0",Gi=(s,t,e,i)=>i===void 0?!1:ct(s,t,e,i.getTime()/1e3),bn=new Array(156).join("\0"),Lt=(s,t,e,i)=>i===void 0?!1:(s.write(i+bn,t,e,"utf8"),i.length!==Buffer.byteLength(i)||i.length>e);import{basename as _n}from"node:path";var ft=class s{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(t,e=!1){this.atime=t.atime,this.charset=t.charset,this.comment=t.comment,this.ctime=t.ctime,this.dev=t.dev,this.gid=t.gid,this.global=e,this.gname=t.gname,this.ino=t.ino,this.linkpath=t.linkpath,this.mtime=t.mtime,this.nlink=t.nlink,this.path=t.path,this.size=t.size,this.uid=t.uid,this.uname=t.uname}encode(){let t=this.encodeBody();if(t==="")return Buffer.allocUnsafe(0);let e=Buffer.byteLength(t),i=512*Math.ceil(1+e/512),r=Buffer.allocUnsafe(i);for(let n=0;n<512;n++)r[n]=0;new F({path:("PaxHeader/"+_n(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:e,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(r),r.write(t,512,e,"utf8");for(let n=e+512;n=Math.pow(10,o)&&(o+=1),o+n+r}static parse(t,e,i=!1){return new s(On(Tn(t),e),i)}},On=(s,t)=>t?Object.assign({},t,s):s,Tn=s=>s.replace(/\n$/,"").split(` +`).reduce(xn,Object.create(null)),xn=(s,t)=>{let e=parseInt(t,10);if(e!==Buffer.byteLength(t)+1)return s;t=t.slice((e+" ").length);let i=t.split("="),r=i.shift();if(!r)return s;let n=r.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),o=i.join("=").replace(/\0.*/,"");switch(n){case"path":case"linkpath":case"type":case"charset":case"comment":case"gname":case"uname":s[n]=o;break;case"ctime":case"atime":case"mtime":s[n]=new Date(Number(o)*1e3);break;case"size":let h=+o;h>=0&&(s[n]=h);break;case"gid":case"uid":case"dev":case"ino":case"nlink":case"mode":s[n]=+o;break}return s};var Ln=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,f=Ln!=="win32"?s=>String(s):s=>String(s).replaceAll(/\\/g,"/");var $e=class extends A{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(t,e,i){switch(super({}),this.pause(),this.extended=e,this.globalExtended=i,this.header=t,this.remain=t.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=t.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!t.path)throw new Error("no path provided for tar.ReadEntry");this.path=f(t.path),this.mode=t.mode,this.mode&&(this.mode=this.mode&4095),this.uid=t.uid,this.gid=t.gid,this.uname=t.uname,this.gname=t.gname,this.size=this.remain,this.mtime=t.mtime,this.atime=t.atime,this.ctime=t.ctime,this.linkpath=t.linkpath?f(t.linkpath):void 0,this.uname=t.uname,this.gname=t.gname,e&&this.#t(e),i&&this.#t(i,!0)}write(t){let e=t.length;if(e>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,r=this.blockRemain;return this.remain=Math.max(0,i-e),this.blockRemain=Math.max(0,r-e),this.ignore?!0:i>=e?super.write(t):super.write(t.subarray(0,i))}#t(t,e=!1){t.path&&(t.path=f(t.path)),t.linkpath&&(t.linkpath=f(t.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(t).filter(([i,r])=>!(r==null||i==="path"&&e))))}};var Dt=(s,t,e,i={})=>{s.file&&(i.file=s.file),s.cwd&&(i.cwd=s.cwd),i.code=e instanceof Error&&e.code||t,i.tarCode=t,!s.strict&&i.recoverable!==!1?(e instanceof Error&&(i=Object.assign(e,i),e=e.message),s.emit("warn",t,e,i)):e instanceof Error?s.emit("error",Object.assign(e,i)):s.emit("error",Object.assign(new Error(`${t}: ${e}`),i))};var Nn=1024*1024,Xi=Buffer.from([31,139]),qi=Buffer.from([40,181,47,253]),An=Math.max(Xi.length,qi.length),B=Symbol("state"),Nt=Symbol("writeEntry"),it=Symbol("readEntry"),Zi=Symbol("nextEntry"),Zs=Symbol("processEntry"),V=Symbol("extendedHeader"),he=Symbol("globalExtendedHeader"),dt=Symbol("meta"),Ys=Symbol("emitMeta"),p=Symbol("buffer"),st=Symbol("queue"),mt=Symbol("ended"),Yi=Symbol("emittedEnd"),At=Symbol("emit"),y=Symbol("unzip"),Xe=Symbol("consumeChunk"),qe=Symbol("consumeChunkSub"),Ki=Symbol("consumeBody"),Ks=Symbol("consumeMeta"),Vs=Symbol("consumeHeader"),ae=Symbol("consuming"),Vi=Symbol("bufferConcat"),Qe=Symbol("maybeEnd"),Yt=Symbol("writing"),$=Symbol("aborted"),Je=Symbol("onDone"),It=Symbol("sawValidEntry"),je=Symbol("sawNullBlock"),ti=Symbol("sawEOF"),$s=Symbol("closeStream"),In=1e3,le=Symbol("compressedBytesRead"),$i=Symbol("decompressedBytesRead"),Xs=Symbol("checkDecompressionRatio"),Cn=()=>!0,rt=class extends Dn{file;strict;maxMetaEntrySize;filter;brotli;zstd;maxDecompressionRatio;writable=!0;readable=!1;[st]=[];[p];[it];[Nt];[B]="begin";[dt]="";[V];[he];[mt]=!1;[y];[$]=!1;[It];[je]=!1;[ti]=!1;[Yt]=!1;[ae]=!1;[Yi]=!1;[le]=0;[$i]=0;constructor(t={}){super(),this.file=t.file||"",this.on(Je,()=>{(this[B]==="begin"||this[It]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),t.ondone?this.on(Je,t.ondone):this.on(Je,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!t.strict,this.maxDecompressionRatio=typeof t.maxDecompressionRatio=="number"?t.maxDecompressionRatio:In,this.maxMetaEntrySize=t.maxMetaEntrySize||Nn,this.filter=typeof t.filter=="function"?t.filter:Cn;let e=t.file&&(t.file.endsWith(".tar.br")||t.file.endsWith(".tbr"));this.brotli=!(t.gzip||t.zstd)&&t.brotli!==void 0?t.brotli:e?void 0:!1;let i=t.file&&(t.file.endsWith(".tar.zst")||t.file.endsWith(".tzst"));this.zstd=!(t.gzip||t.brotli)&&t.zstd!==void 0?t.zstd:i?!0:void 0,this.on("end",()=>this[$s]()),typeof t.onwarn=="function"&&this.on("warn",t.onwarn),typeof t.onReadEntry=="function"&&this.on("entry",t.onReadEntry)}warn(t,e,i={}){Dt(this,t,e,i)}[Vs](t,e){this[It]===void 0&&(this[It]=!1);let i;try{i=new F(t,e,this[V],this[he])}catch(r){return this.warn("TAR_ENTRY_INVALID",r)}if(i.nullBlock)this[je]?(this[ti]=!0,this[B]==="begin"&&(this[B]="header"),this[At]("eof")):(this[je]=!0,this[At]("nullBlock"));else if(this[je]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let r=i.type;if(/^(Symbolic)?Link$/.test(r)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(r)&&!/^(Global)?ExtendedHeader$/.test(r)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let n=this[Nt]=new $e(i,this[V],this[he]);if(!this[It])if(n.remain){let o=()=>{n.invalid||(this[It]=!0)};n.on("end",o)}else this[It]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[At]("ignoredEntry",n),this[B]="ignore",n.resume()):n.size>0&&(this[dt]="",n.on("data",o=>this[dt]+=o),this[B]="meta"):(this[V]=void 0,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[At]("ignoredEntry",n),this[B]=n.remain?"ignore":"header",n.resume()):(n.remain?this[B]="body":(this[B]="header",n.end()),this[it]?this[st].push(n):(this[st].push(n),this[Zi]())))}}}[$s](){queueMicrotask(()=>this.emit("close"))}[Zs](t){let e=!0;if(!t)this[it]=void 0,e=!1;else if(Array.isArray(t)){let[i,...r]=t;this.emit(i,...r)}else this[it]=t,this.emit("entry",t),t.emittedEnd||(t.on("end",()=>this[Zi]()),e=!1);return e}[Zi](){do;while(this[Zs](this[st].shift()));if(this[st].length===0){let t=this[it];!t||t.flowing||t.size===t.remain?this[Yt]||this.emit("drain"):t.once("drain",()=>this.emit("drain"))}}[Ki](t,e){let i=this[Nt];if(!i)throw new Error("attempt to consume body without entry??");let r=i.blockRemain??0,n=r>=t.length&&e===0?t:t.subarray(e,e+r);return i.write(n),i.blockRemain||(this[B]="header",this[Nt]=void 0,i.end()),n.length}[Ks](t,e){let i=this[Nt],r=this[Ki](t,e);return!this[Nt]&&i&&this[Ys](i),r}[At](t,e,i){this[st].length===0&&!this[it]?this.emit(t,e,i):this[st].push([t,e,i])}[Ys](t){switch(this[At]("meta",this[dt]),t.type){case"ExtendedHeader":case"OldExtendedHeader":this[V]=ft.parse(this[dt],this[V],!1);break;case"GlobalExtendedHeader":this[he]=ft.parse(this[dt],this[he],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let e=this[V]??Object.create(null);this[V]=e,e.path=this[dt].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let e=this[V]||Object.create(null);this[V]=e,e.linkpath=this[dt].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+t.type)}}abort(t){this[$]||(this[$]=!0,this.emit("abort",t),this.warn("TAR_ABORT",t,{recoverable:!1}))}[Xs](t){this[$i]+=t.length;let e=this[$i]/this[le];return e>this.maxDecompressionRatio?(this.abort(new Error(`max decompression ratio exceeded: ${e.toFixed(2)} > ${this.maxDecompressionRatio}`)),!1):!0}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this[$])return i?.(),!1;if((this[y]===void 0||this.brotli===void 0&&this[y]===!1)&&t){if(this[p]&&(t=Buffer.concat([this[p],t]),this[p]=void 0),t.length{this[Xs](c)&&this[Xe](c)}),this[y].on("error",c=>{this[$]||this.abort(c)}),this[y].on("end",()=>{this[mt]=!0,this[Xe]()}),this[Yt]=!0,this[le]+=t.length;let l=!!this[y][a?"end":"write"](t);return this[Yt]=!1,i?.(),l}}this[Yt]=!0,this[y]?(this[le]+=t.length,this[y].write(t)):this[Xe](t),this[Yt]=!1;let n=this[st].length>0?!1:this[it]?this[it].flowing:!0;return!n&&this[st].length===0&&this[it]?.once("drain",()=>this.emit("drain")),i?.(),n}[Vi](t){t&&!this[$]&&(this[p]=this[p]?Buffer.concat([this[p],t]):t)}[Qe](){if(this[mt]&&!this[Yi]&&!this[$]&&!this[ae]){this[Yi]=!0;let t=this[Nt];if(t?.blockRemain){let e=this[p]?this[p].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${t.blockRemain} more bytes, only ${e} available)`,{entry:t}),this[p]&&t.write(this[p]),t.end()}this[At](Je)}}[Xe](t){if(this[ae]&&t)this[Vi](t);else if(!t&&!this[p])this[Qe]();else if(t){if(this[ae]=!0,this[p]){this[Vi](t);let e=this[p];this[p]=void 0,this[qe](e)}else this[qe](t);for(;this[p]&&this[p]?.length>=512&&!this[$]&&!this[ti];){let e=this[p];this[p]=void 0,this[qe](e)}this[ae]=!1}(!this[p]||this[mt])&&this[Qe]()}[qe](t){let e=0,i=t.length;for(;e+512<=i&&!this[$]&&!this[ti];)switch(this[B]){case"begin":case"header":this[Vs](t,e),e+=512;break;case"ignore":case"body":e+=this[Ki](t,e);break;case"meta":e+=this[Ks](t,e);break;default:throw new Error("invalid state: "+this[B])}e{let t=s.length-1,e=-1;for(;t>-1&&s.charAt(t)==="/";)e=t,t--;return e===-1?s:s.slice(0,e)};var vn=s=>{let t=s.onReadEntry;s.onReadEntry=t?e=>{t(e),e.resume()}:e=>e.resume()},Qi=(s,t)=>{let e=new Map(t.map(n=>[ut(n),!0])),i=s.filter,r=(n,o="")=>{let h=o||kn(n).root||".",a;if(n===h)a=!1;else{let l=e.get(n);a=l!==void 0?l:r(Fn(n),h)}return e.set(n,a),a};s.filter=i?(n,o)=>i(n,o)&&r(ut(n)):n=>r(ut(n))},Mn=s=>{let t=new rt(s),e=s.file,i;try{i=Kt.openSync(e,"r");let r=Kt.fstatSync(i),n=s.maxReadSize||16*1024*1024;if(r.size{let e=new rt(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{e.on("error",h),e.on("end",o),Kt.stat(r,(a,l)=>{if(a)h(a);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",h),c.pipe(e)}})})},Ct=K(Mn,Bn,s=>new rt(s),s=>new rt(s),(s,t)=>{t?.length&&Qi(s,t),s.noResume||vn(s)});import ui from"fs";import X from"fs";import js from"path";var Ji=(s,t,e)=>(s&=4095,e&&(s=(s|384)&-19),t&&(s&256&&(s|=64),s&32&&(s|=8),s&4&&(s|=1)),s);import{win32 as Pn}from"node:path";var{isAbsolute:zn,parse:qs}=Pn,ce=s=>{let t="",e=qs(s);for(;zn(s)||e.root;){let i=s.charAt(0)==="/"&&s.slice(0,4)!=="//?/"?"/":e.root;s=s.slice(i.length),t+=i,e=qs(s)}return[t,s]};var ei=["|","<",">","?",":"],ji=ei.map(s=>String.fromCodePoint(61440+Number(s.codePointAt(0)))),Un=new Map(ei.map((s,t)=>[s,ji[t]])),Hn=new Map(ji.map((s,t)=>[s,ei[t]])),ts=s=>ei.reduce((t,e)=>t.split(e).join(Un.get(e)),s),Qs=s=>ji.reduce((t,e)=>t.split(e).join(Hn.get(e)),s);var rr=(s,t)=>t?(s=f(s).replace(/^\.(\/|$)/,""),ut(t)+"/"+s):f(s),Wn=16*1024*1024,tr=Symbol("process"),er=Symbol("file"),ir=Symbol("directory"),is=Symbol("symlink"),sr=Symbol("hardlink"),fe=Symbol("header"),ii=Symbol("read"),ss=Symbol("lstat"),si=Symbol("onlstat"),rs=Symbol("onread"),ns=Symbol("onreadlink"),os=Symbol("openfile"),hs=Symbol("onopenfile"),pt=Symbol("close"),ri=Symbol("mode"),as=Symbol("awaitDrain"),es=Symbol("ondrain"),q=Symbol("prefix"),de=class extends A{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#t=!1;constructor(t,e={}){let i=se(e);super(),this.path=f(t),this.portable=!!i.portable,this.maxReadSize=i.maxReadSize||Wn,this.linkCache=i.linkCache||new Map,this.statCache=i.statCache||new Map,this.preservePaths=!!i.preservePaths,this.cwd=f(i.cwd||process.cwd()),this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.mtime=i.mtime,this.prefix=i.prefix?f(i.prefix):void 0,this.onWriteEntry=i.onWriteEntry,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let r=!1;if(!this.preservePaths){let[o,h]=ce(this.path);o&&typeof h=="string"&&(this.path=h,r=o)}this.win32=!!i.win32||process.platform==="win32",this.win32&&(this.path=Qs(this.path.replaceAll(/\\/g,"/")),t=t.replaceAll(/\\/g,"/")),this.absolute=f(i.absolute||js.resolve(this.cwd,t)),this.path===""&&(this.path="./"),r&&this.warn("TAR_ENTRY_INFO",`stripping ${r} from absolute path`,{entry:this,path:r+this.path});let n=this.statCache.get(this.absolute);n?this[si](n):this[ss]()}warn(t,e,i={}){return Dt(this,t,e,i)}emit(t,...e){return t==="error"&&(this.#t=!0),super.emit(t,...e)}[ss](){X.lstat(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[si](e)})}[si](t){this.statCache.set(this.absolute,t),this.stat=t,t.isFile()||(t.size=0),this.type=Gn(t),this.emit("stat",t),this[tr]()}[tr](){switch(this.type){case"File":return this[er]();case"Directory":return this[ir]();case"SymbolicLink":return this[is]();default:return this.end()}}[ri](t){return Ji(t,this.type==="Directory",this.portable)}[q](t){return rr(t,this.prefix)}[fe](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new F({path:this[q](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[q](this.linkpath):this.linkpath,mode:this[ri](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new ft({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[q](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[q](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let t=this.header?.block;if(!t)throw new Error("failed to encode header");super.write(t)}[ir](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[fe](),this.end()}[is](){X.readlink(this.absolute,(t,e)=>{if(t)return this.emit("error",t);this[ns](e)})}[ns](t){this.linkpath=f(t),this[fe](),this.end()}[sr](t){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=f(js.relative(this.cwd,t)),this.stat.size=0,this[fe](),this.end()}[er](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let t=`${this.stat.dev}:${this.stat.ino}`,e=this.linkCache.get(t);if(e?.indexOf(this.cwd)===0)return this[sr](e);this.linkCache.set(t,this.absolute)}if(this[fe](),this.stat.size===0)return this.end();this[os]()}[os](){X.open(this.absolute,"r",(t,e)=>{if(t)return this.emit("error",t);this[hs](e)})}[hs](t){if(this.fd=t,this.#t)return this[pt]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let e=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(e),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[ii]()}[ii](){let{fd:t,buf:e,offset:i,length:r,pos:n}=this;if(t===void 0||e===void 0)throw new Error("cannot read file without first opening");X.read(t,e,i,r,n,(o,h)=>{if(o)return this[pt](()=>this.emit("error",o));this[rs](h)})}[pt](t=()=>{}){this.fd!==void 0&&X.close(this.fd,t)}[rs](t){if(t<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(t>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[pt](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(t===this.remain)for(let r=t;rthis[es]())}[as](t){this.once("drain",t)}write(t,e,i){if(typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8")),this.blockRemaint?this.emit("error",t):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[ii]()}},ni=class extends de{sync=!0;[ss](){this[si](X.lstatSync(this.absolute))}[is](){this[ns](X.readlinkSync(this.absolute))}[os](){this[hs](X.openSync(this.absolute,"r"))}[ii](){let t=!0;try{let{fd:e,buf:i,offset:r,length:n,pos:o}=this;if(e===void 0||i===void 0)throw new Error("fd and buf must be set in READ method");let h=X.readSync(e,i,r,n,o);this[rs](h),t=!1}finally{if(t)try{this[pt](()=>{})}catch{}}}[as](t){t()}[pt](t=()=>{}){this.fd!==void 0&&X.closeSync(this.fd),t()}},oi=class extends A{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(t,e,i={}){return Dt(this,t,e,i)}constructor(t,e={}){let i=se(e);super(),this.preservePaths=!!i.preservePaths,this.portable=!!i.portable,this.strict=!!i.strict,this.noPax=!!i.noPax,this.noMtime=!!i.noMtime,this.onWriteEntry=i.onWriteEntry,this.readEntry=t;let{type:r}=t;if(r==="Unsupported")throw new Error("writing entry that should be ignored");this.type=r,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=i.prefix,this.path=f(t.path),this.mode=t.mode!==void 0?this[ri](t.mode):void 0,this.uid=this.portable?void 0:t.uid,this.gid=this.portable?void 0:t.gid,this.uname=this.portable?void 0:t.uname,this.gname=this.portable?void 0:t.gname,this.size=t.size,this.mtime=this.noMtime?void 0:i.mtime||t.mtime,this.atime=this.portable?void 0:t.atime,this.ctime=this.portable?void 0:t.ctime,this.linkpath=t.linkpath!==void 0?f(t.linkpath):void 0,typeof i.onwarn=="function"&&this.on("warn",i.onwarn);let n=!1;if(!this.preservePaths){let[h,a]=ce(this.path);h&&typeof a=="string"&&(this.path=a,n=h)}this.remain=t.size,this.blockRemain=t.startBlockSize,this.onWriteEntry?.(this),this.header=new F({path:this[q](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[q](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),n&&this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:this,path:n+this.path}),this.header.encode()&&!this.noPax&&super.write(new ft({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[q](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[q](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let o=this.header?.block;if(!o)throw new Error("failed to encode header");super.write(o),t.pipe(this)}[q](t){return rr(t,this.prefix)}[ri](t){return Ji(t,this.type==="Directory",this.portable)}write(t,e,i){typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,typeof e=="string"?e:"utf8"));let r=t.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(t,i)}end(t,e,i){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof t=="function"&&(i=t,e=void 0,t=void 0),typeof e=="function"&&(i=e,e=void 0),typeof t=="string"&&(t=Buffer.from(t,e??"utf8")),i&&this.once("finish",i),t?super.end(t,i):super.end(i),this}},Gn=s=>s.isFile()?"File":s.isDirectory()?"Directory":s.isSymbolicLink()?"SymbolicLink":"Unsupported";var hi=class s{tail;head;length=0;static create(t=[]){return new s(t)}constructor(t=[]){for(let e of t)this.push(e)}*[Symbol.iterator](){for(let t=this.head;t;t=t.next)yield t.value}removeNode(t){if(t.list!==this)throw new Error("removing node which does not belong to this list");let e=t.next,i=t.prev;return e&&(e.prev=i),i&&(i.next=e),t===this.head&&(this.head=e),t===this.tail&&(this.tail=i),this.length--,t.next=void 0,t.prev=void 0,t.list=void 0,e}unshiftNode(t){if(t===this.head)return;t.list&&t.list.removeNode(t);let e=this.head;t.list=this,t.next=e,e&&(e.prev=t),this.head=t,this.tail||(this.tail=t),this.length++}pushNode(t){if(t===this.tail)return;t.list&&t.list.removeNode(t);let e=this.tail;t.list=this,t.prev=e,e&&(e.next=t),this.tail=t,this.head||(this.head=t),this.length++}push(...t){for(let e=0,i=t.length;e1)i=e;else if(this.head)r=this.head.next,i=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;r;n++)i=t(i,r.value,n),r=r.next;return i}reduceReverse(t,e){let i,r=this.tail;if(arguments.length>1)i=e;else if(this.tail)r=this.tail.prev,i=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;r;n--)i=t(i,r.value,n),r=r.prev;return i}toArray(){let t=new Array(this.length);for(let e=0,i=this.head;i;e++)t[e]=i.value,i=i.next;return t}toArrayReverse(){let t=new Array(this.length);for(let e=0,i=this.tail;i;e++)t[e]=i.value,i=i.prev;return t}slice(t=0,e=this.length){e<0&&(e+=this.length),t<0&&(t+=this.length);let i=new s;if(ethis.length&&(e=this.length);let r=this.head,n=0;for(n=0;r&&nthis.length&&(e=this.length);let r=this.length,n=this.tail;for(;n&&r>e;r--)n=n.prev;for(;n&&r>t;r--,n=n.prev)i.push(n.value);return i}splice(t,e=0,...i){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);let r=this.head;for(let o=0;r&&o1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(t.gzip&&(typeof t.gzip!="object"&&(t.gzip={}),this.portable&&(t.gzip.portable=!0),this.zip=new ze(t.gzip)),t.brotli&&(typeof t.brotli!="object"&&(t.brotli={}),this.zip=new We(t.brotli)),t.zstd&&(typeof t.zstd!="object"&&(t.zstd={}),this.zip=new Ye(t.zstd)),!this.zip)throw new Error("impossible");let e=this.zip;e.on("data",i=>super.write(i)),e.on("end",()=>super.end()),e.on("drain",()=>this[fs]()),this.on("resume",()=>e.resume())}else this.on("drain",this[fs]);this.noDirRecurse=!!t.noDirRecurse,this.follow=!!t.follow,this.noMtime=!!t.noMtime,t.mtime&&(this.mtime=t.mtime),this.filter=typeof t.filter=="function"?t.filter:()=>!0,this[W]=new hi,this[G]=0,this.jobs=Number(t.jobs)||4,this[Ee]=!1,this[ue]=!1}[lr](t){return super.write(t)}add(t){return this.write(t),this}end(t,e,i){return typeof t=="function"&&(i=t,t=void 0),typeof e=="function"&&(i=e,e=void 0),t&&this.add(t),this[ue]=!0,this[Ft](),i&&i(),this}write(t){if(this[ue])throw new Error("write after end");return typeof t=="string"?this[ci](t):this[or](t),this.flowing}[or](t){let e=f(ar.resolve(this.cwd,t.path));if(!this.filter(t.path,t))t.resume();else{let i=new pi(t.path,e);i.entry=new oi(t,this[cs](i)),i.entry.on("end",()=>this[ls](i)),this[G]+=1,this[W].push(i)}this[Ft]()}[ci](t){let e=f(ar.resolve(this.cwd,t));this[W].push(new pi(t,e)),this[Ft]()}[ds](t){t.pending=!0,this[G]+=1;let e=this.follow?"stat":"lstat";ui[e](t.absolute,(i,r)=>{t.pending=!1,this[G]-=1,i?this.emit("error",i):this[li](t,r)})}[li](t,e){if(this.statCache.set(t.absolute,e),t.stat=e,!this.filter(t.path,e))t.ignore=!0;else if(e.isFile()&&e.nlink>1&&!this.linkCache.get(`${e.dev}:${e.ino}`)&&!this.sync)if(t===this[Et])this[ai](t);else{let i=`${e.dev}:${e.ino}`,r=this[pe].get(i);r?r.push(t):this[pe].set(i,[t]),t.pendingLink=!0,t.pending=!0}this[Ft]()}[ms](t){t.pending=!0,this[G]+=1,ui.readdir(t.absolute,(e,i)=>{if(t.pending=!1,this[G]-=1,e)return this.emit("error",e);this[fi](t,i)})}[fi](t,e){this.readdirCache.set(t.absolute,e),t.readdir=e,this[Ft]()}[Ft](){if(!this[Ee]){this[Ee]=!0;for(let t=this[W].head;t&&this[G]1){let i=`${e.dev}:${e.ino}`,r=this[pe].get(i);if(r){this[pe].delete(i);for(let n of r)n.pending=!1,this[ai](n)}}this[Ft]()}[ai](t){if(t.pending&&t.pendingLink&&t===this[Et]&&(t.pending=!1,t.pendingLink=!1),!t.pending){if(t.entry){t===this[Et]&&!t.piped&&this[di](t);return}if(!t.stat){let e=this.statCache.get(t.absolute);e?this[li](t,e):this[ds](t)}if(t.stat&&!t.ignore){if(!this.noDirRecurse&&t.stat.isDirectory()&&!t.readdir){let e=this.readdirCache.get(t.absolute);if(e?this[fi](t,e):this[ms](t),!t.readdir)return}if(t.entry=this[hr](t),!t.entry){t.ignore=!0;return}t===this[Et]&&!t.piped&&this[di](t)}}}[cs](t){return{onwarn:(e,i,r)=>this.warn(e,i,r),noPax:this.noPax,cwd:this.cwd,absolute:t.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[hr](t){this[G]+=1;try{return new this[mi](t.path,this[cs](t)).on("end",()=>this[ls](t)).on("error",i=>this.emit("error",i))}catch(e){this.emit("error",e)}}[fs](){this[Et]&&this[Et].entry&&this[Et].entry.resume()}[di](t){t.piped=!0,t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[ci](o+r)});let e=t.entry,i=this.zip;if(!e)throw new Error("cannot pipe without source");i?e.on("data",r=>{i.write(r)||e.pause()}):e.on("data",r=>{super.write(r)||e.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(t,e,i={}){Dt(this,t,e,i)}},kt=class extends wt{sync=!0;constructor(t){super(t),this[mi]=ni}pause(){}resume(){}[ds](t){let e=this.follow?"statSync":"lstatSync";this[li](t,ui[e](t.absolute))}[ms](t){this[fi](t,ui.readdirSync(t.absolute))}[di](t){let e=t.entry,i=this.zip;if(t.readdir&&t.readdir.forEach(r=>{let n=t.path,o=n==="./"?"":n.replace(/\/*$/,"/");this[ci](o+r)}),!e)throw new Error("Cannot pipe without source");i?e.on("data",r=>{i.write(r)}):e.on("data",r=>{super[lr](r)})}};var Vn=(s,t)=>{let e=new kt(s),i=new Wt(s.file,{mode:s.mode||438});e.pipe(i),fr(e,t)},$n=(s,t)=>{let e=new wt(s),i=new et(s.file,{mode:s.mode||438});e.pipe(i);let r=new Promise((n,o)=>{i.on("error",o),i.on("close",n),e.on("error",o)});return dr(e,t).catch(n=>e.emit("error",n)),r},fr=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?Ct({file:cr.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},dr=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await Ct({file:cr.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>{s.add(i)}}):s.add(e);s.end()},Xn=(s,t)=>{let e=new kt(s);return fr(e,t),e},qn=(s,t)=>{let e=new wt(s);return dr(e,t).catch(i=>e.emit("error",i)),e},Qn=K(Vn,$n,Xn,qn,(s,t)=>{if(!t?.length)throw new TypeError("no paths specified to add to archive")});import Br from"node:fs";import co from"node:assert";import{randomBytes as Mr}from"node:crypto";import m from"node:fs";import R from"node:path";import pr from"fs";var Jn=process.env.__FAKE_PLATFORM__||process.platform,Er=Jn==="win32",{O_CREAT:wr,O_NOFOLLOW:mr,O_TRUNC:Sr,O_WRONLY:yr}=pr.constants,Rr=Number(process.env.__FAKE_FS_O_FILENAME__)||pr.constants.UV_FS_O_FILEMAP||0,jn=Er&&!!Rr,to=512*1024,eo=Rr|Sr|wr|yr,ur=!Er&&typeof mr=="number"?mr|Sr|wr|yr:null,us=ur!==null?()=>ur:jn?s=>s"w";import wi from"node:fs";import we from"node:path";var ps=(s,t,e)=>{try{return wi.lchownSync(s,t,e)}catch(i){if(i?.code!=="ENOENT")throw i}},Ei=(s,t,e,i)=>{wi.lchown(s,t,e,r=>{i(r&&r?.code!=="ENOENT"?r:null)})},io=(s,t,e,i,r)=>{if(t.isDirectory())Es(we.resolve(s,t.name),e,i,n=>{if(n)return r(n);let o=we.resolve(s,t.name);Ei(o,e,i,r)});else{let n=we.resolve(s,t.name);Ei(n,e,i,r)}},Es=(s,t,e,i)=>{wi.readdir(s,{withFileTypes:!0},(r,n)=>{if(r){if(r.code==="ENOENT")return i();if(r.code!=="ENOTDIR"&&r.code!=="ENOTSUP")return i(r)}if(r||!n.length)return Ei(s,t,e,i);let o=n.length,h=null,a=l=>{if(!h){if(l)return i(h=l);if(--o===0)return Ei(s,t,e,i)}};for(let l of n)io(s,l,t,e,a)})},so=(s,t,e,i)=>{t.isDirectory()&&ws(we.resolve(s,t.name),e,i),ps(we.resolve(s,t.name),e,i)},ws=(s,t,e)=>{let i;try{i=wi.readdirSync(s,{withFileTypes:!0})}catch(r){let n=r;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return ps(s,t,e);throw n}for(let r of i)so(s,r,t,e);return ps(s,t,e)};import k from"node:fs";import ro from"node:fs/promises";import Si from"node:path";var Se=class extends Error{path;code;syscall="chdir";constructor(t,e){super(`${e}: Cannot cd into '${t}'`),this.path=t,this.code=e}get name(){return"CwdError"}};var St=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(t,e){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=t,this.path=e}get name(){return"SymlinkError"}};var no=(s,t)=>{k.stat(s,(e,i)=>{(e||!i.isDirectory())&&(e=new Se(s,e?.code||"ENOTDIR")),t(e)})},gr=(s,t,e)=>{s=f(s);let i=t.umask??18,r=t.mode|448,n=(r&i)!==0,o=t.uid,h=t.gid,a=typeof o=="number"&&typeof h=="number"&&(o!==t.processUid||h!==t.processGid),l=t.preserve,c=t.unlink,d=f(t.cwd),S=(E,x)=>{E?e(E):x&&a?Es(x,o,h,Le=>S(Le)):n?k.chmod(s,r,e):e()};if(s===d)return no(s,S);if(l)return ro.mkdir(s,{mode:r,recursive:!0}).then(E=>S(null,E??void 0),S);let D=f(Si.relative(d,s)).split("/");Ss(d,D,r,c,d,void 0,S)},Ss=(s,t,e,i,r,n,o)=>{if(t.length===0)return o(null,n);let h=t.shift(),a=f(Si.resolve(s+"/"+h));k.mkdir(a,e,br(a,t,e,i,r,n,o))},br=(s,t,e,i,r,n,o)=>h=>{h?k.lstat(s,(a,l)=>{if(a)a.path=a.path&&f(a.path),o(a);else if(l.isDirectory())Ss(s,t,e,i,r,n,o);else if(i)k.unlink(s,c=>{if(c)return o(c);k.mkdir(s,e,br(s,t,e,i,r,n,o))});else{if(l.isSymbolicLink())return o(new St(s,s+"/"+t.join("/")));o(h)}}):(n=n||s,Ss(s,t,e,i,r,n,o))},oo=s=>{let t=!1,e;try{t=k.statSync(s).isDirectory()}catch(i){e=i?.code}finally{if(!t)throw new Se(s,e??"ENOTDIR")}},_r=(s,t)=>{s=f(s);let e=t.umask??18,i=t.mode|448,r=(i&e)!==0,n=t.uid,o=t.gid,h=typeof n=="number"&&typeof o=="number"&&(n!==t.processUid||o!==t.processGid),a=t.preserve,l=t.unlink,c=f(t.cwd),d=E=>{E&&h&&ws(E,n,o),r&&k.chmodSync(s,i)};if(s===c)return oo(c),d();if(a)return d(k.mkdirSync(s,{mode:i,recursive:!0})??void 0);let T=f(Si.relative(c,s)).split("/"),D;for(let E=T.shift(),x=c;E&&(x+="/"+E);E=T.shift()){x=f(Si.resolve(x));try{k.mkdirSync(x,i),D=D||x}catch{let Le=k.lstatSync(x);if(Le.isDirectory())continue;if(l){k.unlinkSync(x),k.mkdirSync(x,i),D=D||x;continue}else if(Le.isSymbolicLink())return new St(x,x+"/"+T.join("/"))}}return d(D)};import{join as xr}from"node:path";var ys=Object.create(null),Or=1e4,Vt=new Set,Tr=s=>{Vt.has(s)?Vt.delete(s):ys[s]=s.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),Vt.add(s);let t=ys[s],e=Vt.size-Or;if(e>Or/10){for(let i of Vt)if(Vt.delete(i),delete ys[i],--e<=0)break}return t};var ho=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,ao=ho==="win32",lo=s=>s.split("/").slice(0,-1).reduce((e,i)=>{let r=e.at(-1);return r!==void 0&&(i=xr(r,i)),e.push(i||"/"),e},[]),yi=class{#t=new Map;#i=new Map;#s=new Set;reserve(t,e){t=ao?["win32 parallelization disabled"]:t.map(r=>ut(xr(Tr(r))));let i=new Set(t.map(r=>lo(r)).reduce((r,n)=>r.concat(n)));this.#i.set(e,{dirs:i,paths:t});for(let r of t){let n=this.#t.get(r);n?n.push(e):this.#t.set(r,[e])}for(let r of i){let n=this.#t.get(r);if(!n)this.#t.set(r,[new Set([e])]);else{let o=n.at(-1);o instanceof Set?o.add(e):n.push(new Set([e]))}}return this.#r(e)}#n(t){let e=this.#i.get(t);if(!e)throw new Error("function does not have any path reservations");return{paths:e.paths.map(i=>this.#t.get(i)),dirs:[...e.dirs].map(i=>this.#t.get(i))}}check(t){let{paths:e,dirs:i}=this.#n(t);return e.every(r=>r&&r[0]===t)&&i.every(r=>r&&r[0]instanceof Set&&r[0].has(t))}#r(t){return this.#s.has(t)||!this.check(t)?!1:(this.#s.add(t),t(()=>this.#e(t)),!0)}#e(t){if(!this.#s.has(t))return!1;let e=this.#i.get(t);if(!e)throw new Error("invalid reservation");let{paths:i,dirs:r}=e,n=new Set;for(let o of i){let h=this.#t.get(o);if(!h||h?.[0]!==t)continue;let a=h[1];if(!a){this.#t.delete(o);continue}if(h.shift(),typeof a=="function")n.add(a);else for(let l of a)n.add(l)}for(let o of r){let h=this.#t.get(o),a=h?.[0];if(!(!h||!(a instanceof Set)))if(a.size===1&&h.length===1){this.#t.delete(o);continue}else if(a.size===1){h.shift();let l=h[0];typeof l=="function"&&n.add(l)}else a.delete(t)}return this.#s.delete(t),n.forEach(o=>this.#r(o)),!0}};var Lr=()=>process.umask();var Dr=Symbol("onEntry"),_s=Symbol("checkFs"),Nr=Symbol("checkFs2"),Os=Symbol("isReusable"),P=Symbol("makeFs"),Ts=Symbol("file"),xs=Symbol("directory"),gi=Symbol("link"),Ar=Symbol("symlink"),Ir=Symbol("hardlink"),Re=Symbol("ensureNoSymlink"),Cr=Symbol("unsupported"),Fr=Symbol("checkPath"),Rs=Symbol("stripAbsolutePath"),yt=Symbol("mkdir"),O=Symbol("onError"),Ri=Symbol("pending"),kr=Symbol("pend"),$t=Symbol("unpend"),gs=Symbol("ended"),bs=Symbol("maybeClose"),Ls=Symbol("skip"),ge=Symbol("doChown"),be=Symbol("uid"),_e=Symbol("gid"),Oe=Symbol("checkedCwd"),fo=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Te=fo==="win32",mo=1024,uo=(s,t)=>{if(!Te)return m.unlink(s,t);let e=s+".DELETE."+Mr(16).toString("hex");m.rename(s,e,i=>{if(i)return t(i);m.unlink(e,t)})},po=s=>{if(!Te)return m.unlinkSync(s);let t=s+".DELETE."+Mr(16).toString("hex");m.renameSync(s,t),m.unlinkSync(t)},vr=(s,t,e)=>s!==void 0&&s===s>>>0?s:t!==void 0&&t===t>>>0?t:e,Xt=class extends rt{[gs]=!1;[Oe]=!1;[Ri]=0;reservations=new yi;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(t={}){if(t.ondone=()=>{this[gs]=!0,this[bs]()},super(t),this.transform=t.transform,this.chmod=!!t.chmod,typeof t.uid=="number"||typeof t.gid=="number"){if(typeof t.uid!="number"||typeof t.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(t.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=t.uid,this.gid=t.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;this.preserveOwner=t.preserveOwner===void 0&&typeof t.uid!="number"?process.getuid?.()===0:!!t.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof t.maxDepth=="number"?t.maxDepth:mo,this.forceChown=t.forceChown===!0,this.win32=!!t.win32||Te,this.newer=!!t.newer,this.keep=!!t.keep,this.noMtime=!!t.noMtime,this.preservePaths=!!t.preservePaths,this.unlink=!!t.unlink,this.cwd=f(R.resolve(t.cwd||process.cwd())),this.strip=Number(t.strip)||0,this.processUmask=this.chmod?typeof t.processUmask=="number"?t.processUmask:Lr():0,this.umask=typeof t.umask=="number"?t.umask:this.processUmask,this.dmode=t.dmode||511&~this.umask,this.fmode=t.fmode||438&~this.umask,this.on("entry",e=>this[Dr](e))}warn(t,e,i={}){return(t==="TAR_BAD_ARCHIVE"||t==="TAR_ABORT")&&(i.recoverable=!1),super.warn(t,e,i)}[bs](){this[gs]&&this[Ri]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[Rs](t,e){let i=t[e],{type:r}=t;if(!i||this.preservePaths)return!0;let[n,o]=ce(i),h=o.replaceAll(/\\/g,"/").split("/");if(h.includes("..")||Te&&/^[a-z]:\.\.$/i.test(h[0]??"")){if(e==="path"||r==="Link")return this.warn("TAR_ENTRY_ERROR",`${e} contains '..'`,{entry:t,[e]:i}),!1;let a=R.posix.dirname(t.path),l=R.posix.normalize(R.posix.join(a,h.join("/")));if(l.startsWith("../")||l==="..")return this.warn("TAR_ENTRY_ERROR",`${e} escapes extraction directory`,{entry:t,[e]:i}),!1}return n&&(t[e]=String(o),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${e}`,{entry:t,[e]:i})),!0}[Fr](t){let e=f(t.path),i=e.split("/");if(this.strip){if(i.length=this.strip)t.linkpath=r.slice(this.strip).join("/");else return!1}i.splice(0,this.strip),t.path=i.join("/")}if(isFinite(this.maxDepth)&&i.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:t,path:e,depth:i.length,maxDepth:this.maxDepth}),!1;if(!this[Rs](t,"path")||!this[Rs](t,"linkpath"))return!1;if(t.absolute=R.isAbsolute(t.path)?f(R.resolve(t.path)):f(R.resolve(this.cwd,t.path)),!this.preservePaths&&typeof t.absolute=="string"&&t.absolute.indexOf(this.cwd+"/")!==0&&t.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:t,path:f(t.path),resolvedPath:t.absolute,cwd:this.cwd}),!1;if(t.absolute===this.cwd&&t.type!=="Directory"&&t.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=R.win32.parse(String(t.absolute));t.absolute=r+ts(String(t.absolute).slice(r.length));let{root:n}=R.win32.parse(t.path);t.path=n+ts(t.path.slice(n.length))}return!0}[Dr](t){if(!this[Fr](t))return t.resume();switch(co.equal(typeof t.absolute,"string"),t.type){case"Directory":case"GNUDumpDir":t.mode&&(t.mode=t.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[_s](t);default:return this[Cr](t)}}[O](t,e){t.name==="CwdError"?this.emit("error",t):(this.warn("TAR_ENTRY_ERROR",t,{entry:e}),this[$t](),e.resume())}[yt](t,e,i){gr(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e},i)}[ge](t){return this.forceChown||this.preserveOwner&&(typeof t.uid=="number"&&t.uid!==this.processUid||typeof t.gid=="number"&&t.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[be](t){return vr(this.uid,t.uid,this.processUid)}[_e](t){return vr(this.gid,t.gid,this.processGid)}[Ts](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=new et(String(t.absolute),{flags:us(t.size),mode:i,autoClose:!1});r.on("error",a=>{r.fd&&m.close(r.fd,()=>{}),r.write=()=>!0,this[O](a,t),e()});let n=1,o=a=>{if(a){r.fd&&m.close(r.fd,()=>{}),this[O](a,t),e();return}--n===0&&r.fd!==void 0&&m.close(r.fd,l=>{l?this[O](l,t):this[$t](),e()})};r.on("finish",()=>{let a=String(t.absolute),l=r.fd;if(typeof l=="number"&&t.mtime&&!this.noMtime){n++;let c=t.atime||new Date,d=t.mtime;m.futimes(l,c,d,S=>S?m.utimes(a,c,d,T=>o(T&&S)):o())}if(typeof l=="number"&&this[ge](t)){n++;let c=this[be](t),d=this[_e](t);typeof c=="number"&&typeof d=="number"&&m.fchown(l,c,d,S=>S?m.chown(a,c,d,T=>o(T&&S)):o())}o()});let h=this.transform&&this.transform(t)||t;h!==t&&(h.on("error",a=>{this[O](a,t),e()}),t.pipe(h)),h.pipe(r)}[xs](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode;this[yt](String(t.absolute),i,r=>{if(r){this[O](r,t),e();return}let n=1,o=()=>{--n===0&&(e(),this[$t](),t.resume())};t.mtime&&!this.noMtime&&(n++,m.utimes(String(t.absolute),t.atime||new Date,t.mtime,o)),this[ge](t)&&(n++,m.chown(String(t.absolute),Number(this[be](t)),Number(this[_e](t)),o)),o()})}[Cr](t){t.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${t.type}`,{entry:t}),t.resume()}[Ar](t,e){let i=f(R.relative(this.cwd,R.resolve(R.dirname(String(t.absolute)),String(t.linkpath)))).split("/");this[Re](t,this.cwd,i,()=>this[gi](t,String(t.linkpath),"symlink",e),r=>{this[O](r,t),e()})}[Ir](t,e){let i=f(R.resolve(this.cwd,String(t.linkpath))),r=f(String(t.linkpath)).split("/");this[Re](t,this.cwd,r,()=>this[gi](t,i,"link",e),n=>{this[O](n,t),e()})}[Re](t,e,i,r,n){let o=i.shift();if(this.preservePaths||o===void 0)return r();let h=R.resolve(e,o);m.lstat(h,(a,l)=>{if(a)return r();if(l?.isSymbolicLink())return n(new St(h,R.resolve(h,i.join("/"))));this[Re](t,h,i,r,n)})}[kr](){this[Ri]++}[$t](){this[Ri]--,this[bs]()}[Ls](t){this[$t](),t.resume()}[Os](t,e){return t.type==="File"&&!this.unlink&&e.isFile()&&e.nlink<=1&&!Te}[_s](t){this[kr]();let e=[t.path];t.linkpath&&e.push(t.linkpath),this.reservations.reserve(e,i=>this[Nr](t,i))}[Nr](t,e){let i=h=>{e(h)},r=()=>{this[yt](this.cwd,this.dmode,h=>{if(h){this[O](h,t),i();return}this[Oe]=!0,n()})},n=()=>{if(t.absolute!==this.cwd){let h=f(R.dirname(String(t.absolute)));if(h!==this.cwd)return this[yt](h,this.dmode,a=>{if(a){this[O](a,t),i();return}o()})}o()},o=()=>{m.lstat(String(t.absolute),(h,a)=>{if(a&&(this.keep||this.newer&&a.mtime>(t.mtime??a.mtime))){this[Ls](t),i();return}if(h||this[Os](t,a))return this[P](null,t,i);if(a.isDirectory()){if(t.type==="Directory"){let l=this.chmod&&t.mode&&(a.mode&4095)!==t.mode,c=d=>this[P](d??null,t,i);return l?m.chmod(String(t.absolute),Number(t.mode),c):c()}if(t.absolute!==this.cwd)return m.rmdir(String(t.absolute),l=>this[P](l??null,t,i))}if(t.absolute===this.cwd)return this[P](null,t,i);uo(String(t.absolute),l=>this[P](l??null,t,i))})};this[Oe]?n():r()}[P](t,e,i){if(t){this[O](t,e),i();return}switch(e.type){case"File":case"OldFile":case"ContiguousFile":return this[Ts](e,i);case"Link":return this[Ir](e,i);case"SymbolicLink":return this[Ar](e,i);case"Directory":case"GNUDumpDir":return this[xs](e,i)}}[gi](t,e,i,r){m[i](e,String(t.absolute),n=>{n?this[O](n,t):(this[$t](),t.resume()),r()})}},ye=s=>{try{return[null,s()]}catch(t){return[t,null]}},xe=class extends Xt{sync=!0;[P](t,e){return super[P](t,e,()=>{})}[_s](t){if(!this[Oe]){let n=this[yt](this.cwd,this.dmode);if(n)return this[O](n,t);this[Oe]=!0}if(t.absolute!==this.cwd){let n=f(R.dirname(String(t.absolute)));if(n!==this.cwd){let o=this[yt](n,this.dmode);if(o)return this[O](o,t)}}let[e,i]=ye(()=>m.lstatSync(String(t.absolute)));if(i&&(this.keep||this.newer&&i.mtime>(t.mtime??i.mtime)))return this[Ls](t);if(e||this[Os](t,i))return this[P](null,t);if(i.isDirectory()){if(t.type==="Directory"){let o=this.chmod&&t.mode&&(i.mode&4095)!==t.mode,[h]=o?ye(()=>{m.chmodSync(String(t.absolute),Number(t.mode))}):[];return this[P](h,t)}let[n]=ye(()=>m.rmdirSync(String(t.absolute)));this[P](n,t)}let[r]=t.absolute===this.cwd?[]:ye(()=>po(String(t.absolute)));this[P](r,t)}[Ts](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.fmode,r=h=>{let a;try{m.closeSync(n)}catch(l){a=l}(h||a)&&this[O](h||a,t),e()},n;try{n=m.openSync(String(t.absolute),us(t.size),i)}catch(h){return r(h)}let o=this.transform&&this.transform(t)||t;o!==t&&(o.on("error",h=>this[O](h,t)),t.pipe(o)),o.on("data",h=>{try{m.writeSync(n,h,0,h.length)}catch(a){r(a)}}),o.on("end",()=>{let h=null;if(t.mtime&&!this.noMtime){let a=t.atime||new Date,l=t.mtime;try{m.futimesSync(n,a,l)}catch(c){try{m.utimesSync(String(t.absolute),a,l)}catch{h=c}}}if(this[ge](t)){let a=this[be](t),l=this[_e](t);try{m.fchownSync(n,Number(a),Number(l))}catch(c){try{m.chownSync(String(t.absolute),Number(a),Number(l))}catch{h=h||c}}}r(h)})}[xs](t,e){let i=typeof t.mode=="number"?t.mode&4095:this.dmode,r=this[yt](String(t.absolute),i);if(r){this[O](r,t),e();return}if(t.mtime&&!this.noMtime)try{m.utimesSync(String(t.absolute),t.atime||new Date,t.mtime)}catch{}if(this[ge](t))try{m.chownSync(String(t.absolute),Number(this[be](t)),Number(this[_e](t)))}catch{}e(),t.resume()}[yt](t,e){try{return _r(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:e})}catch(i){return i}}[Re](t,e,i,r,n){if(this.preservePaths||i.length===0)return r();let o=e;for(let h of i){o=R.resolve(o,h);let[a,l]=ye(()=>m.lstatSync(o));if(a)return r();if(l.isSymbolicLink())return n(new St(o,R.resolve(e,i.join("/"))))}r()}[gi](t,e,i,r){let n=`${i}Sync`;try{m[n](e,String(t.absolute)),r(),t.resume()}catch(o){return this[O](o,t)}}};var Eo=s=>{let t=new xe(s),e=s.file,i=Br.statSync(e),r=s.maxReadSize||16*1024*1024;new Be(e,{readSize:r,size:i.size}).pipe(t)},wo=(s,t)=>{let e=new Xt(s),i=s.maxReadSize||16*1024*1024,r=s.file;return new Promise((o,h)=>{e.on("error",h),e.on("close",o),Br.stat(r,(a,l)=>{if(a)h(a);else{let c=new _t(r,{readSize:i,size:l.size});c.on("error",h),c.pipe(e)}})})},So=K(Eo,wo,s=>new xe(s),s=>new Xt(s),(s,t)=>{t?.length&&Qi(s,t)});import v from"node:fs";import Pr from"node:path";var yo=(s,t)=>{let e=new kt(s),i=!0,r,n;try{try{r=v.openSync(s.file,"r+")}catch(a){if(a?.code==="ENOENT")r=v.openSync(s.file,"w+");else throw a}let o=v.fstatSync(r),h=Buffer.alloc(512);t:for(n=0;no.size)break;n+=l,s.mtimeCache&&a.mtime&&s.mtimeCache.set(String(a.path),a.mtime)}i=!1,Ro(s,e,n,r,t)}finally{if(i)try{v.closeSync(r)}catch{}}},Ro=(s,t,e,i,r)=>{let n=new Wt(s.file,{fd:i,start:e});t.pipe(n),bo(t,r)},go=(s,t)=>{t=Array.from(t);let e=new wt(s),i=(n,o,h)=>{let a=(T,D)=>{T?v.close(n,E=>h(T)):h(null,D)},l=0;if(o===0)return a(null,0);let c=0,d=Buffer.alloc(512),S=(T,D)=>{if(T||D===void 0)return a(T);if(c+=D,c<512&&D)return v.read(n,d,c,d.length-c,l+c,S);if(l===0&&d[0]===31&&d[1]===139)return a(new Error("cannot append to compressed archives"));if(c<512)return a(null,l);let E=new F(d);if(!E.cksumValid)return a(null,l);let x=512*Math.ceil((E.size??0)/512);if(l+x+512>o||(l+=x+512,l>=o))return a(null,l);s.mtimeCache&&E.mtime&&s.mtimeCache.set(String(E.path),E.mtime),c=0,v.read(n,d,0,512,l,S)};v.read(n,d,0,512,l,S)};return new Promise((n,o)=>{e.on("error",o);let h="r+",a=(l,c)=>{if(l&&l.code==="ENOENT"&&h==="r+")return h="w+",v.open(s.file,h,a);if(l||!c)return o(l);v.fstat(c,(d,S)=>{if(d)return v.close(c,()=>o(d));i(c,S.size,(T,D)=>{if(T)return o(T);let E=new et(s.file,{fd:c,start:D});e.pipe(E),E.on("error",o),E.on("close",n),_o(e,t)})})};v.open(s.file,h,a)})},bo=(s,t)=>{t.forEach(e=>{e.charAt(0)==="@"?Ct({file:Pr.resolve(s.cwd,e.slice(1)),sync:!0,noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e)}),s.end()},_o=async(s,t)=>{for(let e of t)e.charAt(0)==="@"?await Ct({file:Pr.resolve(String(s.cwd),e.slice(1)),noResume:!0,onReadEntry:i=>s.add(i)}):s.add(e);s.end()},vt=K(yo,go,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(s,t)=>{if(!Bs(s))throw new TypeError("file is required");if(s.gzip||s.brotli||s.zstd||s.file.endsWith(".br")||s.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!t?.length)throw new TypeError("no paths specified to add/replace")});var Oo=K(vt.syncFile,vt.asyncFile,vt.syncNoFile,vt.asyncNoFile,(s,t=[])=>{vt.validate?.(s,t),To(s)}),To=s=>{let t=s.filter;s.mtimeCache||(s.mtimeCache=new Map),s.filter=t?(e,i)=>t(e,i)&&!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0)):(e,i)=>!((s.mtimeCache?.get(e)??i.mtime??0)>(i.mtime??0))};export{F as Header,wt as Pack,pi as PackJob,kt as PackSync,rt as Parser,ft as Pax,$e as ReadEntry,Xt as Unpack,xe as UnpackSync,de as WriteEntry,ni as WriteEntrySync,oi as WriteEntryTar,Qn as c,Qn as create,So as extract,Qi as filesFilter,Ct as list,vt as r,vt as replace,Ct as t,Hi as types,Oo as u,Oo as update,So as x}; //# sourceMappingURL=index.min.js.map diff --git a/deps/npm/node_modules/tar/dist/esm/normalize-windows-path.js b/deps/npm/node_modules/tar/dist/esm/normalize-windows-path.js index bde0c962344a30..cc75178aa9cee4 100644 --- a/deps/npm/node_modules/tar/dist/esm/normalize-windows-path.js +++ b/deps/npm/node_modules/tar/dist/esm/normalize-windows-path.js @@ -4,6 +4,6 @@ // so that we can use / as our one and only directory separator char. const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform; export const normalizeWindowsPath = platform !== 'win32' ? - (p) => p - : (p) => p && p.replaceAll(/\\/g, '/'); + (p) => String(p) + : (p) => String(p).replaceAll(/\\/g, '/'); //# sourceMappingURL=normalize-windows-path.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/tar/dist/esm/parse.js b/deps/npm/node_modules/tar/dist/esm/parse.js index 02454bfd2c4052..fb303a5171bea0 100644 --- a/deps/npm/node_modules/tar/dist/esm/parse.js +++ b/deps/npm/node_modules/tar/dist/esm/parse.js @@ -57,6 +57,10 @@ const SAW_VALID_ENTRY = Symbol('sawValidEntry'); const SAW_NULL_BLOCK = Symbol('sawNullBlock'); const SAW_EOF = Symbol('sawEOF'); const CLOSESTREAM = Symbol('closeStream'); +const MAX_DECOMPRESSION_RATIO = 1000; +const COMPRESSEDBYTESREAD = Symbol('compressedBytesRead'); +const DECOMPRESSEDBYTESREAD = Symbol('decompressedBytesRead'); +const CHECKDECOMPRESSIONRATIO = Symbol('checkDecompressionRatio'); const noop = () => true; export class Parser extends EE { file; @@ -65,6 +69,7 @@ export class Parser extends EE { filter; brotli; zstd; + maxDecompressionRatio; writable = true; readable = false; [QUEUE] = []; @@ -84,6 +89,8 @@ export class Parser extends EE { [WRITING] = false; [CONSUMING] = false; [EMITTEDEND] = false; + [COMPRESSEDBYTESREAD] = 0; + [DECOMPRESSEDBYTESREAD] = 0; constructor(opt = {}) { super(); this.file = opt.file || ''; @@ -106,6 +113,10 @@ export class Parser extends EE { }); } this.strict = !!opt.strict; + this.maxDecompressionRatio = + typeof opt.maxDecompressionRatio === 'number' ? + opt.maxDecompressionRatio + : MAX_DECOMPRESSION_RATIO; this.maxMetaEntrySize = opt.maxMetaEntrySize || maxMetaEntrySize; this.filter = typeof opt.filter === 'function' ? opt.filter : noop; // Unlike gzip, brotli doesn't have any magic bytes to identify it @@ -359,11 +370,23 @@ export class Parser extends EE { } } abort(error) { + if (this[ABORTED]) { + return; + } this[ABORTED] = true; this.emit('abort', error); // always throws, even in non-strict mode this.warn('TAR_ABORT', error, { recoverable: false }); } + [CHECKDECOMPRESSIONRATIO](chunk) { + this[DECOMPRESSEDBYTESREAD] += chunk.length; + const ratio = this[DECOMPRESSEDBYTESREAD] / this[COMPRESSEDBYTESREAD]; + if (ratio > this.maxDecompressionRatio) { + this.abort(new Error(`max decompression ratio exceeded: ${ratio.toFixed(2)} > ${this.maxDecompressionRatio}`)); + return false; + } + return true; + } write(chunk, encoding, cb) { if (typeof encoding === 'function') { cb = encoding; @@ -447,13 +470,22 @@ export class Parser extends EE { this[UNZIP] === undefined ? new Unzip({}) : isZstd ? new ZstdDecompress({}) : new BrotliDecompress({}); - this[UNZIP].on('data', chunk => this[CONSUMECHUNK](chunk)); - this[UNZIP].on('error', er => this.abort(er)); + this[UNZIP].on('data', chunk => { + if (this[CHECKDECOMPRESSIONRATIO](chunk)) { + this[CONSUMECHUNK](chunk); + } + }); + this[UNZIP].on('error', er => { + if (!this[ABORTED]) { + this.abort(er); + } + }); this[UNZIP].on('end', () => { this[ENDED] = true; this[CONSUMECHUNK](); }); this[WRITING] = true; + this[COMPRESSEDBYTESREAD] += chunk.length; const ret = !!this[UNZIP][ended ? 'end' : 'write'](chunk); this[WRITING] = false; cb?.(); @@ -462,6 +494,7 @@ export class Parser extends EE { } this[WRITING] = true; if (this[UNZIP]) { + this[COMPRESSEDBYTESREAD] += chunk.length; this[UNZIP].write(chunk); } else { @@ -492,7 +525,7 @@ export class Parser extends EE { !this[CONSUMING]) { this[EMITTEDEND] = true; const entry = this[WRITEENTRY]; - if (entry && entry.blockRemain) { + if (entry?.blockRemain) { // truncated, likely a damaged file const have = this[BUFFER] ? this[BUFFER].length : 0; this.warn('TAR_BAD_ARCHIVE', `Truncated input (needed ${entry.blockRemain} more bytes, only ${have} available)`, { entry }); @@ -586,8 +619,10 @@ export class Parser extends EE { if (!this[ABORTED]) { if (this[UNZIP]) { /* c8 ignore start */ - if (chunk) + if (chunk) { + this[COMPRESSEDBYTESREAD] += chunk.length; this[UNZIP].write(chunk); + } /* c8 ignore stop */ this[UNZIP].end(); } diff --git a/deps/npm/node_modules/tar/dist/esm/pax.js b/deps/npm/node_modules/tar/dist/esm/pax.js index 832808f344da53..907193a4b93257 100644 --- a/deps/npm/node_modules/tar/dist/esm/pax.js +++ b/deps/npm/node_modules/tar/dist/esm/pax.js @@ -143,12 +143,36 @@ const parseKVLine = (set, line) => { return set; } const k = r.replace(/^SCHILY\.(dev|ino|nlink)/, '$1'); - const v = kv.join('='); - set[k] = - /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) ? - new Date(Number(v) * 1000) - : /^[0-9]+$/.test(v) ? +v - : v; + const v = kv.join('=').replace(/\0.*/, ''); + switch (k) { + case 'path': + case 'linkpath': + case 'type': + case 'charset': + case 'comment': + case 'gname': + case 'uname': + set[k] = v; + break; + case 'ctime': + case 'atime': + case 'mtime': + set[k] = new Date(Number(v) * 1000); + break; + case 'size': + const s = +v; + if (s >= 0) + set[k] = s; + break; + case 'gid': + case 'uid': + case 'dev': + case 'ino': + case 'nlink': + case 'mode': + set[k] = +v; + break; + } return set; }; //# sourceMappingURL=pax.js.map \ No newline at end of file diff --git a/deps/npm/node_modules/tar/dist/esm/unpack.js b/deps/npm/node_modules/tar/dist/esm/unpack.js index 46c53db2ad57c6..3bbdad99bcd8a0 100644 --- a/deps/npm/node_modules/tar/dist/esm/unpack.js +++ b/deps/npm/node_modules/tar/dist/esm/unpack.js @@ -146,7 +146,7 @@ export class Unpack extends Parser { // default true for root this.preserveOwner = opt.preserveOwner === undefined && typeof opt.uid !== 'number' ? - !!(process.getuid && process.getuid() === 0) + !!(process.getuid?.() === 0) : !!opt.preserveOwner; this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ? @@ -367,7 +367,7 @@ export class Unpack extends Parser { } } [MKDIR](dir, mode, cb) { - mkdir(normalizeWindowsPath(dir), { + void mkdir(normalizeWindowsPath(dir), { uid: this.uid, gid: this.gid, processUid: this.processUid, diff --git a/deps/npm/node_modules/tar/package.json b/deps/npm/node_modules/tar/package.json index 556b3e7d9f2fb0..9d082e44049477 100644 --- a/deps/npm/node_modules/tar/package.json +++ b/deps/npm/node_modules/tar/package.json @@ -2,7 +2,7 @@ "author": "Isaac Z. Schlueter", "name": "tar", "description": "tar for node", - "version": "7.5.16", + "version": "7.5.19", "repository": { "type": "git", "url": "https://github.com/isaacs/node-tar.git" diff --git a/deps/npm/node_modules/undici/docs/docs/api/Client.md b/deps/npm/node_modules/undici/docs/docs/api/Client.md index fdee5ea702671b..e0d41b47805ec2 100644 --- a/deps/npm/node_modules/undici/docs/docs/api/Client.md +++ b/deps/npm/node_modules/undici/docs/docs/api/Client.md @@ -27,6 +27,7 @@ Returns: `Client` * **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB. * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. * **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. + * **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit. * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. 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 ef3d38ea4f2ed3..9455517a19b15f 100644 --- a/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js +++ b/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js @@ -57,6 +57,9 @@ const EMPTY_BUF = Buffer.alloc(0) const FastBuffer = Buffer[Symbol.species] const addListener = util.addListener const removeAllListeners = util.removeAllListeners +const kIdleSocketValidation = Symbol('kIdleSocketValidation') +const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout') +const kSocketUsed = Symbol('kSocketUsed') let extractBody @@ -371,6 +374,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] if (!request) { return -1 @@ -474,6 +482,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] /* istanbul ignore next: difficult to make a test case for */ @@ -647,6 +660,7 @@ class Parser { request.onComplete(headers) client[kQueue][client[kRunningIdx]++] = null + socket[kSocketUsed] = true if (socket[kWriting]) { assert(client[kRunning] === 0) @@ -705,6 +719,9 @@ async function connectH1 (client, socket) { socket[kWriting] = false socket[kReset] = false socket[kBlocking] = false + socket[kIdleSocketValidation] = 0 + socket[kIdleSocketValidationTimeout] = null + socket[kSocketUsed] = false socket[kParser] = new Parser(client, socket, llhttpInstance) addListener(socket, 'error', function (err) { @@ -751,6 +768,8 @@ async function connectH1 (client, socket) { const client = this[kClient] const parser = this[kParser] + clearIdleSocketValidation(this) + if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { this[kError] = parser.finish() || this[kError] @@ -816,7 +835,7 @@ async function connectH1 (client, socket) { return socket.destroyed }, busy (request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true } @@ -854,6 +873,31 @@ async function connectH1 (client, socket) { } } +function clearIdleSocketValidation (socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearTimeout(socket[kIdleSocketValidationTimeout]) + socket[kIdleSocketValidationTimeout] = null + } + + socket[kIdleSocketValidation] = 0 +} + +function scheduleIdleSocketValidation (client, socket) { + socket[kIdleSocketValidation] = 1 + socket[kIdleSocketValidationTimeout] = setTimeout(() => { + socket[kIdleSocketValidationTimeout] = null + socket[kIdleSocketValidation] = 2 + + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume]() + } + }, 0) + socket[kIdleSocketValidationTimeout].unref?.() +} + +/** + * @param {import('./client.js')} client + */ function resumeH1 (client) { const socket = client[kSocket] @@ -868,6 +912,32 @@ function resumeH1 (client) { socket[kNoRef] = false } + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket) + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + } + + if (client[kRunning] === 0) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + } + if (client[kSize] === 0) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE) @@ -961,6 +1031,7 @@ function writeH1 (client, request) { } const socket = client[kSocket] + clearIdleSocketValidation(socket) const abort = (err) => { if (request.aborted || request.completed) { diff --git a/deps/npm/node_modules/undici/lib/dispatcher/dispatcher-base.js b/deps/npm/node_modules/undici/lib/dispatcher/dispatcher-base.js index c999b2c2fb6740..371a3ea1f6f1e0 100644 --- a/deps/npm/node_modules/undici/lib/dispatcher/dispatcher-base.js +++ b/deps/npm/node_modules/undici/lib/dispatcher/dispatcher-base.js @@ -26,6 +26,7 @@ class DispatcherBase extends Dispatcher { get webSocketOptions () { return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 } } diff --git a/deps/npm/node_modules/undici/lib/web/cookies/parse.js b/deps/npm/node_modules/undici/lib/web/cookies/parse.js index 3c48c26b93ffa0..b3c25fb9423e29 100644 --- a/deps/npm/node_modules/undici/lib/web/cookies/parse.js +++ b/deps/npm/node_modules/undici/lib/web/cookies/parse.js @@ -275,32 +275,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) // If the attribute-name case-insensitively matches the string // "SameSite", the user agent MUST process the cookie-av as follows: - // 1. Let enforcement be "Default". - let enforcement = 'Default' - const attributeValueLowercase = attributeValue.toLowerCase() - // 2. If cookie-av's attribute-value is a case-insensitive match for - // "None", set enforcement to "None". - if (attributeValueLowercase.includes('none')) { - enforcement = 'None' - } - // 3. If cookie-av's attribute-value is a case-insensitive match for - // "Strict", set enforcement to "Strict". - if (attributeValueLowercase.includes('strict')) { - enforcement = 'Strict' + // 1. If cookie-av's attribute-value is a case-insensitive match for + // "None", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "None". + if (attributeValueLowercase === 'none') { + cookieAttributeList.sameSite = 'None' + } else if (attributeValueLowercase === 'strict') { + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", append an attribute to the cookie-attribute-list with + // an attribute-name of "SameSite" and an attribute-value of + // "Strict". + cookieAttributeList.sameSite = 'Strict' + } else if (attributeValueLowercase === 'lax') { + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "Lax". + cookieAttributeList.sameSite = 'Lax' } - - // 4. If cookie-av's attribute-value is a case-insensitive match for - // "Lax", set enforcement to "Lax". - if (attributeValueLowercase.includes('lax')) { - enforcement = 'Lax' - } - - // 5. Append an attribute to the cookie-attribute-list with an - // attribute-name of "SameSite" and an attribute-value of - // enforcement. - cookieAttributeList.sameSite = enforcement } else { cookieAttributeList.unparsed ??= [] diff --git a/deps/npm/node_modules/undici/lib/web/websocket/receiver.js b/deps/npm/node_modules/undici/lib/web/websocket/receiver.js index 53e427eb2e4642..a7dea7fae1c1fc 100644 --- a/deps/npm/node_modules/undici/lib/web/websocket/receiver.js +++ b/deps/npm/node_modules/undici/lib/web/websocket/receiver.js @@ -20,6 +20,11 @@ const { closeWebSocketConnection } = require('./connection') const { PerMessageDeflate } = require('./permessage-deflate') const { MessageSizeExceededError } = require('../../core/errors') +function failWebsocketConnectionWithCode (ws, code, reason) { + closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason)) + failWebsocketConnection(ws, reason) +} + // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik // Copyright (c) 2013 Arnout Kazemier and contributors @@ -39,19 +44,23 @@ class ByteParser extends Writable { /** @type {Map} */ #extensions + /** @type {number} */ + #maxFragments + /** @type {number} */ #maxPayloadSize /** * @param {import('./websocket').WebSocket} ws * @param {Map|null} extensions - * @param {{ maxPayloadSize?: number }} [options] + * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options] */ constructor (ws, extensions, options = {}) { super() this.ws = ws this.#extensions = extensions == null ? new Map() : extensions + this.#maxFragments = options.maxFragments ?? 0 this.#maxPayloadSize = options.maxPayloadSize ?? 0 if (this.#extensions.has('permessage-deflate')) { @@ -75,9 +84,9 @@ class ByteParser extends Writable { if ( this.#maxPayloadSize > 0 && !isControlFrame(this.#info.opcode) && - this.#info.payloadLength > this.#maxPayloadSize + this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize ) { - failWebsocketConnection(this.ws, 'Payload size exceeds maximum allowed size') + failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size') return false } @@ -242,10 +251,12 @@ class ByteParser extends Writable { this.#state = parserStates.INFO } else { if (!this.#info.compressed) { - this.writeFragments(body) + if (!this.writeFragments(body)) { + return + } if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { - failWebsocketConnection(this.ws, new MessageSizeExceededError().message) + failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message) return } @@ -264,14 +275,17 @@ class ByteParser extends Writable { this.#info.fin, (error, data) => { if (error) { - failWebsocketConnection(this.ws, error.message) + const code = error instanceof MessageSizeExceededError ? 1009 : 1007 + failWebsocketConnectionWithCode(this.ws, code, error.message) return } - this.writeFragments(data) + if (!this.writeFragments(data)) { + return + } if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { - failWebsocketConnection(this.ws, new MessageSizeExceededError().message) + failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message) return } @@ -341,8 +355,17 @@ class ByteParser extends Writable { } writeFragments (fragment) { + if ( + this.#maxFragments > 0 && + this.#fragments.length === this.#maxFragments + ) { + failWebsocketConnectionWithCode(this.ws, 1008, 'Too many message fragments') + return false + } + this.#fragmentsBytes += fragment.length this.#fragments.push(fragment) + return true } consumeFragments () { diff --git a/deps/npm/node_modules/undici/lib/web/websocket/websocket.js b/deps/npm/node_modules/undici/lib/web/websocket/websocket.js index ccedb792169a10..80991e96a2e94f 100644 --- a/deps/npm/node_modules/undici/lib/web/websocket/websocket.js +++ b/deps/npm/node_modules/undici/lib/web/websocket/websocket.js @@ -435,9 +435,12 @@ class WebSocket extends EventTarget { // once this happens, the connection is open this[kResponse] = response - const maxPayloadSize = this[kController]?.dispatcher?.webSocketOptions?.maxPayloadSize + const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions + const maxFragments = webSocketOptions?.maxFragments + const maxPayloadSize = webSocketOptions?.maxPayloadSize const parser = new ByteParser(this, parsedExtensions, { + maxFragments, maxPayloadSize }) parser.on('drain', onParserDrain) diff --git a/deps/npm/node_modules/undici/package.json b/deps/npm/node_modules/undici/package.json index d1eef502c4169f..3c2001f391e3f8 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.26.0", + "version": "6.27.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 3309782d55e6db..b2ee14b3db8b6f 100644 --- a/deps/npm/package.json +++ b/deps/npm/package.json @@ -1,5 +1,5 @@ { - "version": "11.17.0", + "version": "11.18.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.8.0", - "@npmcli/config": "^10.11.0", + "@npmcli/arborist": "^9.9.0", + "@npmcli/config": "^10.12.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -77,11 +77,11 @@ "is-cidr": "^6.0.4", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.10", - "libnpmexec": "^10.3.0", - "libnpmfund": "^7.0.24", + "libnpmdiff": "^8.1.11", + "libnpmexec": "^10.3.1", + "libnpmfund": "^7.0.25", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.10", + "libnpmpack": "^9.1.11", "libnpmpublish": "^11.2.0", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", @@ -97,7 +97,7 @@ "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.2", "npm-pick-manifest": "^11.0.3", - "npm-profile": "^12.0.1", + "npm-profile": "^12.0.2", "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", @@ -106,11 +106,11 @@ "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", "read": "^5.0.1", - "semver": "^7.8.4", + "semver": "^7.8.5", "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.16", + "tar": "^7.5.19", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", 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 143d08dda8ff4b..dbc6b9f13b0ac4 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 @@ -181,6 +181,7 @@ Object { "man/man1/npm-help.1", "man/man1/npm-init.1", "man/man1/npm-install-ci-test.1", + "man/man1/npm-install-scripts.1", "man/man1/npm-install-test.1", "man/man1/npm-install.1", "man/man1/npm-link.1", 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 c5fd6a2ecfe036..1e2799652c91fe 100644 --- a/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs @@ -125,6 +125,7 @@ Array [ "init", "install", "install-ci-test", + "install-scripts", "install-test", "link", "ll", @@ -404,6 +405,10 @@ sources, the standard precedence applies (cli > env > project > user > global), so a higher-priority source can always relax or override a lower-priority one. +As with \`min-release-age\`, when this cutoff blocks a fix that \`npm audit +fix\` would install, npm keeps the vulnerable version, warns, and exits with +a non-zero code. + Packages whose names match \`min-release-age-exclude\` are exempt from this filter. @@ -1084,8 +1089,16 @@ Sets the strategy for installing packages in node_modules. hoisted (default): Install non-duplicated in top-level, and duplicated as necessary within directory structure. nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct -deps at top-level. linked: (experimental) install in node_modules/.store, -link in place, unhoisted. +deps at top-level. linked: install in node_modules/.store, link in place, +unhoisted. + +We recommend that package authors use \`--install-strategy=linked\` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +\`import\` of a package that was never added to \`package.json\` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). @@ -1279,6 +1292,12 @@ your \`.npmrc\` is preserved when npm internally spawns a sub-process with apply, \`before\` wins within a single source and across sources the standard precedence rules apply. +When this window stops \`npm audit fix\` from installing a patched version +(because the fix was published too recently), npm keeps the package at its +vulnerable version, warns that the fix was blocked, and exits with a +non-zero code. To install the fix, add the package to +\`min-release-age-exclude\`, or relax \`min-release-age\` or \`before\`. + Packages whose names match \`min-release-age-exclude\` are exempt from this filter. @@ -1675,7 +1694,14 @@ registry (https://registry.npmjs.org) to the configured registry. If set to "never", then use the registry value. If set to "always", then replace the registry host with the configured host every time. -You may also specify a bare hostname (e.g., "registry.npmjs.org"). +You may also specify a bare hostname (e.g., "registry.npmjs.org") to only +replace URLs coming from that host. + +You may also specify a full URL including a path (e.g., +"https://old-registry.example.com/npm/path"). In that case, resolved URLs +whose host and path begin with that prefix will have the entire prefix +replaced with the configured registry URL (host and path), without +duplicating path segments. @@ -1932,6 +1958,10 @@ silently skipped; this setting only affects unreviewed entries. \`--ignore-scripts\` and \`--dangerously-allow-all-scripts\` both override this setting. +Optional dependencies that cannot be installed on the current platform or +engine (a non-matching \`os\`, \`cpu\`, or \`libc\`) are not flagged, because +their install scripts never run. + #### \`strict-peer-deps\` @@ -4573,6 +4603,52 @@ aliases: cit, clean-install-test, sit #### \`install-links\` ` +exports[`test/lib/docs.js TAP usage install-scripts > must match snapshot 1`] = ` +Manage install-script approvals for dependencies + +Usage: +npm install-scripts approve [ ...] +npm install-scripts approve --all +npm install-scripts deny [ ...] +npm install-scripts deny --all +npm install-scripts ls +npm install-scripts prune + +Options: +[-a|--all] [--no-allow-scripts-pin] [--dry-run] [--json] + + -a|--all + Show or act on all packages, not just the ones your project directly + + --allow-scripts-pin + Write pinned (\`pkg@version\`) entries when approving install scripts. + + --dry-run + Indicates that you don't want npm to make any changes and that it should + + --json + Whether or not to output JSON data, rather than the normal output. + + +Run "npm help install-scripts" for more info + +\`\`\`bash +npm install-scripts approve [ ...] +npm install-scripts approve --all +npm install-scripts deny [ ...] +npm install-scripts deny --all +npm install-scripts ls +npm install-scripts prune +\`\`\` + +Note: This command is unaware of workspaces. + +#### \`all\` +#### \`allow-scripts-pin\` +#### \`dry-run\` +#### \`json\` +` + exports[`test/lib/docs.js TAP usage install-test > must match snapshot 1`] = ` Install package(s) and run tests 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 dc1c95cd3c5763..a3837dc72ab761 100644 --- a/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs @@ -35,12 +35,13 @@ All commands: 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 + install-ci-test, install-scripts, 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} @@ -81,6 +82,7 @@ All commands: help-search, init, install, install-ci-test, + install-scripts, install-test, link, ll, login, logout, ls, org, outdated, owner, pack, @@ -136,6 +138,7 @@ All commands: help-search, init, install, install-ci-test, + install-scripts, install-test, link, ll, login, logout, ls, org, outdated, owner, pack, @@ -182,12 +185,13 @@ All commands: 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 + install-ci-test, install-scripts, 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} @@ -228,6 +232,7 @@ All commands: help-search, init, install, install-ci-test, + install-scripts, install-test, link, ll, login, logout, ls, org, outdated, owner, pack, @@ -283,6 +288,7 @@ All commands: help-search, init, install, install-ci-test, + install-scripts, install-test, link, ll, login, logout, ls, org, outdated, owner, pack, @@ -337,6 +343,7 @@ All commands: fund, get, help, help-search, init, install, install-ci-test, + install-scripts, install-test, link, ll, login, logout, ls, org, outdated, owner, pack, @@ -383,12 +390,13 @@ All commands: 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 + install-ci-test, install-scripts, 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} @@ -420,12 +428,13 @@ All commands: 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 + install-ci-test, install-scripts, 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} @@ -457,12 +466,13 @@ All commands: 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 + install-ci-test, install-scripts, 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/tap-snapshots/test/lib/utils/reify-output.js.test.cjs b/deps/npm/tap-snapshots/test/lib/utils/reify-output.js.test.cjs index 40d4cd221ca024..828a99679eab6b 100644 --- a/deps/npm/tap-snapshots/test/lib/utils/reify-output.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/utils/reify-output.js.test.cjs @@ -10,6 +10,11 @@ exports[`test/lib/utils/reify-output.js TAP added packages should be looked up w added 1 package in {TIME} ` +exports[`test/lib/utils/reify-output.js TAP added packages should be looked up within returned tree linked store package counted though absent from actualTree > must match snapshot 1`] = ` + +added 1 package in {TIME} +` + exports[`test/lib/utils/reify-output.js TAP added packages should be looked up within returned tree missing added pkg in inventory > must match snapshot 1`] = ` up to date in {TIME} diff --git a/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs b/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs index 124478bc829938..6963299566d53d 100644 --- a/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs @@ -417,7 +417,7 @@ exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - from git url > must "version": "1.0.0", "scope": "required", "author": "Author", - "purl": "pkg:npm/root@1.0.0?vcs_url=https://github.com/foo/bar#1234", + "purl": "pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar%231234", "properties": [], "externalReferences": [ { diff --git a/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs b/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs index 6adb6d26de1435..eda01bcaa4ed6a 100644 --- a/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs @@ -414,7 +414,7 @@ exports[`test/lib/utils/sbom-spdx.js TAP single node - from git url > must match { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", - "referenceLocator": "pkg:npm/root@1.0.0?vcs_url=https://github.com/foo/bar#1234" + "referenceLocator": "pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar%231234" } ] } diff --git a/deps/npm/test/lib/commands/approve-scripts.js b/deps/npm/test/lib/commands/approve-scripts.js index 30325f41014c88..4b2e7e5a73e1f8 100644 --- a/deps/npm/test/lib/commands/approve-scripts.js +++ b/deps/npm/test/lib/commands/approve-scripts.js @@ -7,7 +7,7 @@ const mockNpm = async (t, opts = {}) => { return _mockNpm(t, opts) } -const setupProject = ({ allowScripts, withScripts = ['canvas'] } = {}) => { +const setupProject = ({ allowScripts, withScripts = ['canvas'], noResolved = [] } = {}) => { const pkg = { name: 'host', version: '1.0.0', @@ -28,11 +28,16 @@ const setupProject = ({ allowScripts, withScripts = ['canvas'] } = {}) => { scripts: { install: 'echo install' }, }), } - lockPackages[`node_modules/${name}`] = { + const lockEntry = { version: '1.0.0', - resolved: tarUrl, hasInstallScript: true, } + // Some lockfiles omit `resolved` for registry deps. Those nodes have no + // trustable version, so they can only be approved by name. + if (!noResolved.includes(name)) { + lockEntry.resolved = tarUrl + } + lockPackages[`node_modules/${name}`] = lockEntry } return { @@ -119,6 +124,51 @@ t.test('approve-scripts --all approves every unreviewed package', async t => { }) }) +t.test('approve-scripts --all approves a dep without a resolved URL by name', async t => { + // Regression for npm/cli#9558: a dep with no `resolved` URL can't be + // pinned, but must still be approved by name, not silently skipped. + const { npm, prefix, logs } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'], noResolved: ['canvas'] }), + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { canvas: true }, 'approved by name, not skipped') + t.match( + logs.warn.byTitle('approve-scripts').join('\n'), + /no "resolved" URL/, + 'warns that a version pin could not be written' + ) +}) + +t.test('approve-scripts approves a dep without a resolved URL by name', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'], noResolved: ['canvas'] }), + }) + 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 --pending is empty after a no-resolved dep is approved by name', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ + allowScripts: { canvas: true }, + withScripts: ['canvas'], + noResolved: ['canvas'], + }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + t.match( + joinedOutput(), + /No packages with unreviewed install scripts/, + 'name-only entry covers the dep even without a resolved URL' + ) +}) + t.test('approve-scripts errors on unknown package', async t => { const { npm } = await mockNpm(t, { prefixDir: setupProject({ withScripts: ['canvas'] }), diff --git a/deps/npm/test/lib/commands/audit.js b/deps/npm/test/lib/commands/audit.js index 3bd5e5033c499f..61887417216eb0 100644 --- a/deps/npm/test/lib/commands/audit.js +++ b/deps/npm/test/lib/commands/audit.js @@ -163,6 +163,86 @@ t.test('audit fix - bulk endpoint', async t => { ) }) +t.test('audit fix exits non-zero when min-release-age blocks a fix', async t => { + const { npm, logs } = await loadMockNpm(t, { + prefixDir: tree, + config: { 'min-release-age': 30 }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + const manifest = registry.manifest({ + name: 'test-dep-a', + packuments: [{ version: '1.0.0' }, { version: '1.0.1' }], + }) + // 1.0.0 is old enough to install; the fix 1.0.1 was published too recently. + manifest.time['1.0.0'] = '2020-01-01T00:00:00.000Z' + manifest.time['1.0.1'] = new Date().toISOString() + await registry.package({ + manifest, + tarballs: { + '1.0.0': path.join(npm.prefix, 'test-dep-a-vuln'), + }, + times: 2, + }) + const advisory = registry.advisory({ id: 100, vulnerable_versions: '<1.0.1' }) + registry.nock.post('/-/npm/v1/security/advisories/bulk', body => { + const unzipped = JSON.parse(gunzip(Buffer.from(body, 'hex'))) + return t.same(unzipped, { 'test-dep-a': ['1.0.0'] }) + }) + .reply(200, { 'test-dep-a': [advisory] }) + .post('/-/npm/v1/security/advisories/bulk', body => { + const unzipped = JSON.parse(gunzip(Buffer.from(body, 'hex'))) + return t.same(unzipped, { 'test-dep-a': ['1.0.0'] }) + }) + .reply(200, { 'test-dep-a': [advisory] }) + + await npm.exec('audit', ['fix']) + + t.equal(process.exitCode, 1, 'exits non-zero because the fix was blocked') + t.ok( + logs.warn.some(w => + /left at a vulnerable version because a fix is newer than the release-age cutoff/.test(w)), + 'warns that the fix was blocked by min-release-age' + ) + const lock = JSON.parse(fs.readFileSync(path.join(npm.prefix, 'package-lock.json'), 'utf8')) + t.equal(lock.packages['node_modules/test-dep-a'].version, '1.0.0', + 'test-dep-a was left at the vulnerable version') +}) + +t.test('json audit reports fixBlockedByReleaseAge when a fix is too new', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir: tree, + config: { + json: true, + 'min-release-age': 30, + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + const manifest = registry.manifest({ + name: 'test-dep-a', + packuments: [{ version: '1.0.0' }, { version: '1.0.1' }], + }) + manifest.time['1.0.0'] = '2020-01-01T00:00:00.000Z' + manifest.time['1.0.1'] = new Date().toISOString() + await registry.package({ manifest }) + const advisory = registry.advisory({ id: 100, vulnerable_versions: '<1.0.1' }) + const bulkBody = gzip(JSON.stringify({ 'test-dep-a': ['1.0.0'] })) + registry.nock.post('/-/npm/v1/security/advisories/bulk', bulkBody) + .reply(200, { + 'test-dep-a': [advisory], + }) + + await npm.exec('audit', []) + const report = JSON.parse(joinedOutput()) + t.match(report.vulnerabilities['test-dep-a'].fixBlockedByReleaseAge, { version: '1.0.1' }, + 'json output flags the fix that min-release-age blocked') +}) + t.test('audit fix no package lock', async t => { const { npm } = await loadMockNpm(t, { config: { diff --git a/deps/npm/test/lib/commands/deny-scripts.js b/deps/npm/test/lib/commands/deny-scripts.js index 358b71d7da7bd6..1caf93b715134c 100644 --- a/deps/npm/test/lib/commands/deny-scripts.js +++ b/deps/npm/test/lib/commands/deny-scripts.js @@ -80,7 +80,10 @@ t.test('deny-scripts --pending is rejected', async t => { prefixDir: setupProject({ withScripts: ['core-js'] }), config: { 'allow-scripts-pending': true }, }) - await t.rejects(npm.exec('deny-scripts', []), { code: 'EUSAGE' }) + await t.rejects(npm.exec('deny-scripts', []), { + code: 'EUSAGE', + message: /`npm deny-scripts --allow-scripts-pending` is not supported; run `npm install-scripts ls` to list unreviewed packages/, + }) }) t.test('deny-scripts --all denies every unreviewed package', async t => { diff --git a/deps/npm/test/lib/commands/exec.js b/deps/npm/test/lib/commands/exec.js index 92ea993e3edfb2..45e7634da5e100 100644 --- a/deps/npm/test/lib/commands/exec.js +++ b/deps/npm/test/lib/commands/exec.js @@ -220,6 +220,41 @@ t.test('finds workspace dep first', async t => { t.ok(exists.isFile(), 'bin ran, creating file') }) +t.test('finds workspace dep bin under linked install strategy', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'install-strategy': 'linked', + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-root-test', + workspaces: ['workspace-a', 'tool'], + }), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + dependencies: { tool: '*' }, + }), + }, + tool: { + 'package.json': JSON.stringify({ + name: 'tool', + version: '1.0.0', + bin: { 'npx-test': 'index.js' }, + }), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, + }, + }, + }) + + await npm.exec('install', []) + npm.config.set('workspace', ['workspace-a']) + await npm.exec('exec', ['npx-test']) + const exists = await fs.stat(path.join(npm.prefix, 'workspace-a', 'npm-exec-test-success')) + t.ok(exists.isFile(), 'workspace-local bin ran instead of falling back to the registry') +}) + t.test('npx --no-install @npmcli/npx-test', async t => { const registry = new MockRegistry({ tap: t, diff --git a/deps/npm/test/lib/commands/install-scripts.js b/deps/npm/test/lib/commands/install-scripts.js new file mode 100644 index 00000000000000..523aba42bdd943 --- /dev/null +++ b/deps/npm/test/lib/commands/install-scripts.js @@ -0,0 +1,336 @@ +const t = require('tap') +const fs = require('node:fs') +const { resolve } = require('node:path') +const _mockNpm = require('../../fixtures/mock-npm') +const InstallScripts = require('../../../lib/commands/install-scripts.js') + +const mockNpm = async (t, opts = {}) => { + return _mockNpm(t, opts) +} + +const setupProject = ({ allowScripts, withScripts = ['canvas'], noScripts = [] } = {}) => { + const pkg = { + name: 'host', + version: '1.0.0', + dependencies: Object.fromEntries([...withScripts, ...noScripts].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', + hasInstallScript: true, + resolved: `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz`, + } + } + for (const name of noScripts) { + nodeModules[name] = { + 'package.json': JSON.stringify({ name, version: '1.0.0' }), + } + lockPackages[`node_modules/${name}`] = { + version: '1.0.0', + resolved: `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz`, + } + } + + 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('completion', async t => { + const comp = (argv) => + InstallScripts.completion({ conf: { argv: { remain: argv } } }) + + t.resolveMatch(comp(['npm', 'install-scripts']), ['approve', 'deny', 'ls', 'prune']) + t.resolveMatch(comp(['npm', 'install-scripts', 'approve']), []) + t.resolveMatch(comp(['npm', 'install-scripts', 'deny']), []) + t.resolveMatch(comp(['npm', 'install-scripts', 'ls']), []) + t.resolveMatch(comp(['npm', 'install-scripts', 'prune']), []) + await t.rejects(comp(['npm', 'install-scripts', 'frobnicate']), { + message: 'frobnicate not recognized', + }) +}) + +t.test('install-scripts approve writes a pinned entry', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await npm.exec('install-scripts', ['approve', 'canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) +}) + +t.test('install-scripts approve --all approves every unreviewed package', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas', 'sharp'] }), + config: { all: true }, + }) + await npm.exec('install-scripts', ['approve']) + + 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('install-scripts deny writes a name-only false entry', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await npm.exec('install-scripts', ['deny', 'canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { canvas: false }) +}) + +t.test('install-scripts deny --all denies every unreviewed package', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas', 'sharp'] }), + config: { all: true }, + }) + await npm.exec('install-scripts', ['deny']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { canvas: false, sharp: false }) +}) + +t.test('install-scripts ignores allow-scripts-pending and still writes', async t => { + // The namespace exposes listing through `ls`, so a stray + // `allow-scripts-pending` config must not divert approve into list mode. + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('install-scripts', ['approve', 'canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) +}) + +t.test('install-scripts ls lists unreviewed packages', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas', 'sharp'] }), + }) + await npm.exec('install-scripts', ['ls']) + const out = joinedOutput() + t.match(out, /2 packages have install scripts not yet covered by allowScripts/) + t.match(out, /canvas@1\.0\.0/) + t.match(out, /sharp@1\.0\.0/) +}) + +t.test('install-scripts ls with no unreviewed says so', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ allowScripts: { canvas: true }, withScripts: ['canvas'] }), + }) + await npm.exec('install-scripts', ['ls']) + t.match(joinedOutput(), /No packages with unreviewed install scripts/) +}) + +t.test('install-scripts ls rejects positional args', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects( + npm.exec('install-scripts', ['ls', 'canvas']), + /cannot be combined with positional arguments/ + ) +}) + +t.test('install-scripts with no subcommand errors with usage', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects( + npm.exec('install-scripts', []), + { code: 'EUSAGE' } + ) +}) + +t.test('install-scripts with an unknown subcommand errors', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects( + npm.exec('install-scripts', ['frobnicate']), + /`frobnicate` is not a recognized subcommand/ + ) +}) + +t.test('install-scripts approve errors on unknown package', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects( + npm.exec('install-scripts', ['approve', 'not-installed']), + { code: 'ENOMATCH' } + ) +}) + +t.test('install-scripts fails for global installs', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { global: true }, + }) + await t.rejects( + npm.exec('install-scripts', ['approve', 'canvas']), + { code: 'EGLOBAL' } + ) +}) + +t.test('install-scripts prune removes not-installed and no-script entries', async t => { + const { npm, prefix, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + noScripts: ['no-scripts-pkg'], + allowScripts: { + 'canvas@1.0.0': true, + 'no-scripts-pkg': true, + gone: true, + }, + }), + }) + await npm.exec('install-scripts', ['prune']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) + + const out = joinedOutput() + t.match(out, /Removed 2 unused allowScripts entries:/) + t.match(out, /no-scripts-pkg \(no install scripts\)/) + t.match(out, /gone \(package not installed\)/) +}) + +t.test('install-scripts prune removes unused deny entries too', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { 'canvas@1.0.0': true, 'denied-gone': false }, + }), + }) + await npm.exec('install-scripts', ['prune']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) +}) + +t.test('install-scripts prune removes a stale version pin and drops the field', async t => { + const { npm, prefix, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { 'canvas@9.9.9': true }, + }), + }) + await npm.exec('install-scripts', ['prune']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.notOk('allowScripts' in pkg, 'allowScripts field is removed when empty') + // Singular wording for a single entry. + t.match(joinedOutput(), /Removed 1 unused allowScripts entry:/) +}) + +t.test('install-scripts prune --dry-run reports without writing', async t => { + const allowScripts = { 'canvas@1.0.0': true, gone: true } + const { npm, prefix, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'], allowScripts }), + config: { 'dry-run': true }, + }) + await npm.exec('install-scripts', ['prune']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, allowScripts, 'package.json is unchanged') + t.match(joinedOutput(), /Would remove 1 unused allowScripts entry:/) +}) + +t.test('install-scripts prune --json emits a machine-readable summary', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { 'canvas@1.0.0': true, gone: true }, + }), + config: { json: true }, + }) + await npm.exec('install-scripts', ['prune']) + + t.strictSame(JSON.parse(joinedOutput()), { + allowScripts: { + removed: [{ key: 'gone', value: true, reason: 'not-installed' }], + dryRun: false, + }, + }) +}) + +t.test('install-scripts prune with nothing unused says so', async t => { + const { npm, prefix, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { 'canvas@1.0.0': true }, + }), + }) + await npm.exec('install-scripts', ['prune']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) + t.match(joinedOutput(), /No unused allowScripts entries\./) +}) + +t.test('install-scripts prune with no allowScripts field says so', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await npm.exec('install-scripts', ['prune']) + t.match(joinedOutput(), /No unused allowScripts entries\./) +}) + +t.test('install-scripts prune rejects positional args', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects( + npm.exec('install-scripts', ['prune', 'canvas']), + /cannot be combined with positional arguments/ + ) +}) + +t.test('install-scripts prune rejects --all', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { all: true }, + }) + await t.rejects( + npm.exec('install-scripts', ['prune']), + /cannot be combined with positional arguments or `--all`/ + ) +}) + +t.test('install-scripts prune fails for global installs', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { global: true }, + }) + await t.rejects( + npm.exec('install-scripts', ['prune']), + { code: 'EGLOBAL' } + ) +}) diff --git a/deps/npm/test/lib/commands/link.js b/deps/npm/test/lib/commands/link.js index 4aaf24c9d0a287..11ed182edd94ba 100644 --- a/deps/npm/test/lib/commands/link.js +++ b/deps/npm/test/lib/commands/link.js @@ -289,6 +289,44 @@ t.test('link global linked pkg to local workspace using args', async t => { t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') }) +t.test('link --workspace --save targets the workspace manifest, not the root', async t => { + const { link, prefix } = await mockLink(t, { + globalPrefixDir: { + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + }, + }, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: 'my-project', + version: '1.0.0', + workspaces: ['packages/*'], + }), + packages: { + x: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.0.0', + }), + }, + }, + }, + config: { workspace: 'x', save: true }, + }) + + await link.exec(['a']) + + const root = JSON.parse(fs.readFileSync(join(prefix, 'package.json'), 'utf8')) + const ws = JSON.parse(fs.readFileSync(join(prefix, 'packages', 'x', 'package.json'), 'utf8')) + t.notOk(root.dependencies, 'root manifest should not get the dependency') + t.match(ws.dependencies, { a: /^file:/ }, 'workspace manifest should get the file: dependency') +}) + t.test('link pkg already in global space', async t => { const { npm, link, printLinks, prefix } = await mockLink(t, { globalPrefixDir: { @@ -554,3 +592,73 @@ t.test('link threads allowScripts policy through to arborist', async t => { t.strictSame(localOpts.allowScripts, { canvas: true }, 'local arborist opts.allowScripts populated from package.json') }) + +t.test('link threads allowScripts policy to the global install', async t => { + const capturedOpts = [] + const FakeArborist = function (opts) { + capturedOpts.push(opts) + this.options = opts + this.actualTree = { inventory: new Map() } + } + FakeArborist.prototype.loadActual = async () => ({ isLink: false, children: new Map() }) + FakeArborist.prototype.reify = async () => {} + + const mock = await mockNpm(t, { + command: 'link', + 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('link', ['canvas']) + // the global Arborist is constructed first; its missing-package install + // must carry the project policy. + t.strictSame(capturedOpts[0].allowScripts, { canvas: true }, + 'global arborist opts.allowScripts populated from package.json') +}) + +t.test('link runs the strict-allow-scripts preflight before the global install', async t => { + // The mocked preflight stands in for strict mode finding an uncovered + // script. It must run and throw before the global reify. + const calls = [] + const FakeArborist = function (opts) { + this.options = opts + this.actualTree = { inventory: new Map() } + } + FakeArborist.prototype.loadActual = async () => ({ isLink: false, children: new Map() }) + FakeArborist.prototype.reify = async () => { + calls.push('reify') + } + + const mock = await mockNpm(t, { + command: 'link', + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + }), + }, + mocks: { + '@npmcli/arborist': FakeArborist, + '{LIB}/utils/reify-finish.js': async () => {}, + '{LIB}/utils/strict-allow-scripts-preflight.js': async () => { + calls.push('preflight') + throw Object.assign(new Error('blocked'), { code: 'ESTRICTALLOWSCRIPTS' }) + }, + }, + }) + await t.rejects( + mock.npm.exec('link', ['canvas']), + { code: 'ESTRICTALLOWSCRIPTS' }, + 'the strict preflight blocks the link' + ) + t.strictSame(calls, ['preflight'], + 'preflight ran and the global reify never executed') +}) diff --git a/deps/npm/test/lib/commands/login.js b/deps/npm/test/lib/commands/login.js index 55568edd09f9d2..4ebc7147e46ae1 100644 --- a/deps/npm/test/lib/commands/login.js +++ b/deps/npm/test/lib/commands/login.js @@ -130,6 +130,19 @@ t.test('web', t => { }) t.match(outputs[0], '/npm-cli-test/login/cli/00000000-0000-0000-0000-000000000000') }) + t.test('proxy registry whose doneUrl points at the canonical registry', async t => { + // Regression for npm/cli#8875: a proxy/mirror returns a doneUrl on registry.npmjs.org. + // npm must poll the configured proxy where the session lives, not the canonical registry. + const proxy = 'https://proxy.registry.example/' + const { npm, registry, login, rc } = await mockLogin(t, { + registry: proxy, + config: { 'auth-type': 'web', registry: proxy }, + }) + registry.weblogin({ token: 'npm_proxy-token', doneRegistry: 'https://registry.npmjs.org' }) + await login.exec([]) + t.same(npm.config.get('//proxy.registry.example/:_authToken'), 'npm_proxy-token') + t.match(rc(), { '//proxy.registry.example/:_authToken': 'npm_proxy-token' }) + }) t.test('server error', async t => { const { registry, login } = await mockLogin(t, { config: { 'auth-type': 'web' }, diff --git a/deps/npm/test/lib/commands/ls.js b/deps/npm/test/lib/commands/ls.js index ab98773bc68e5c..899a2b8169051b 100644 --- a/deps/npm/test/lib/commands/ls.js +++ b/deps/npm/test/lib/commands/ls.js @@ -5332,6 +5332,25 @@ t.test('ls --install-strategy=linked', async t => { node_modules: { 'workspace-a': t.fixture('symlink', '../packages/workspace-a'), // workspace-b intentionally NOT linked (undeclared in dependencies) + // The hidden lockfile a real linked install writes records only the + // declared workspace as linked into root node_modules, so loadActual + // resolves workspace-b's root edge as missing (the undeclared-workspace case). + '.package-lock.json': JSON.stringify({ + lockfileVersion: 3, + requires: true, + packages: { + 'node_modules/workspace-a': { + resolved: 'packages/workspace-a', + link: true, + }, + 'packages/workspace-a': { + version: '1.0.0', + }, + 'packages/workspace-b': { + version: '1.0.0', + }, + }, + }), }, }, }) @@ -5339,6 +5358,7 @@ t.test('ls --install-strategy=linked', async t => { const output = cleanCwd(result()) t.notMatch(output, /UNMET DEPENDENCY/, 'should not report undeclared workspace as UNMET DEPENDENCY') t.match(output, /workspace-a/, 'should list declared workspace') + t.match(output, /workspace-b/, 'should list undeclared workspace (npm/cli#9618)') }) t.test('should not report devDeps of store packages as UNMET DEPENDENCY', async t => { @@ -5397,7 +5417,20 @@ t.test('ls --install-strategy=linked', async t => { }, }, node_modules: { - // workspace-a is declared but its symlink is missing + // workspace-a is declared but its symlink is missing. + // The hidden lockfile records the workspace target without a root + // node_modules link, so loadActual resolves the declared workspace's + // root edge as missing (the declared-but-missing case), which must + // still be reported as UNMET DEPENDENCY. + '.package-lock.json': JSON.stringify({ + lockfileVersion: 3, + requires: true, + packages: { + 'packages/workspace-a': { + version: '1.0.0', + }, + }, + }), }, }, }) diff --git a/deps/npm/test/lib/commands/query.js b/deps/npm/test/lib/commands/query.js index 40dadc88858363..cb9230c93fbcb3 100644 --- a/deps/npm/test/lib/commands/query.js +++ b/deps/npm/test/lib/commands/query.js @@ -160,6 +160,61 @@ t.test('linked node', async t => { t.matchSnapshot(joinedOutput(), 'should return linked node res') }) +t.test('linked strategy reports logical location, not store backing path', async t => { + /* linked layout: nopt (direct) symlinked to its store key, abbrev a transitive dep of nopt */ + const linkedDir = { + prefixDir: { + node_modules: { + nopt: t.fixture('symlink', '.store/nopt-hash/node_modules/nopt'), + '.store': { + 'nopt-hash': { + node_modules: { + nopt: { + 'package.json': JSON.stringify({ + name: 'nopt', + version: '7.2.1', + dependencies: { abbrev: '^2.0.0' }, + }), + }, + abbrev: t.fixture('symlink', '../../abbrev-hash/node_modules/abbrev'), + }, + }, + 'abbrev-hash': { + node_modules: { + abbrev: { + 'package.json': JSON.stringify({ name: 'abbrev', version: '2.0.0' }), + }, + }, + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'project', + version: '1.0.0', + dependencies: { nopt: '^7.0.0' }, + }), + }, + } + + await t.test(':root > * reports the logical link location', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, linkedDir) + await npm.exec('query', [':root > *']) + const res = JSON.parse(joinedOutput()) + t.equal(res.length, 1, 'returns a single result, not the store node duplicate') + t.equal(res[0].location, 'node_modules/nopt', 'reports the logical location') + t.match(res[0].realpath, /\.store/, 'realpath still resolves to the store') + }) + + await t.test(':root * keeps direct deps logical and transitive deps canonical', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, linkedDir) + await npm.exec('query', [':root *']) + const byName = Object.fromEntries(JSON.parse(joinedOutput()).map(n => [n.name, n.location])) + t.equal(byName.nopt, 'node_modules/nopt', 'direct dep keeps its logical location') + t.equal(byName.abbrev, 'node_modules/.store/abbrev-hash/node_modules/abbrev', + 'transitive dep reports its canonical store key, not a consumer symlink') + }) +}) + t.test('global', async t => { const { npm, joinedOutput } = await loadMockNpm(t, { config: { @@ -434,3 +489,24 @@ t.test('missing', async t => { await npm.exec('query', [':missing']) t.matchSnapshot(joinedOutput(), 'should return missing node') }) + +t.test('linked strategy surfaces undeclared workspaces', async t => { + // npm/cli#9618: undeclared workspaces are not symlinked into root node_modules under linked, but must remain visible to `npm query`. + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { 'install-strategy': 'linked' }, + prefixDir: { + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['packages/*'], + }), + packages: { + a: { 'package.json': JSON.stringify({ name: 'a', version: '1.0.0' }) }, + b: { 'package.json': JSON.stringify({ name: 'b', version: '1.0.0' }) }, + }, + }, + }) + await npm.exec('query', [':root > *']) + const names = JSON.parse(joinedOutput()).map(n => n.name).sort() + t.same(names, ['a', 'b'], 'both undeclared workspaces are listed') +}) diff --git a/deps/npm/test/lib/commands/token.js b/deps/npm/test/lib/commands/token.js index 56ab906d921bec..34297a923c8972 100644 --- a/deps/npm/test/lib/commands/token.js +++ b/deps/npm/test/lib/commands/token.js @@ -13,6 +13,7 @@ const tokens = [ { key: 'abcd1234abcd1234', token: 'efgh5678efgh5678', + name: 'abcd001', cidr_whitelist: null, readonly: false, created: now, @@ -21,6 +22,7 @@ const tokens = [ { key: 'abcd1256', token: 'hgfe8765', + name: 'abcd002', cidr_whitelist: ['192.168.1.1/32'], readonly: true, created: now, @@ -63,9 +65,9 @@ t.test('token list', async t => { registry.getTokens(tokens) await npm.exec('token', []) t.strictSame(outputs, [ - `Token efgh5678efgh5678… with id abcd123 created ${now.slice(0, 10)}`, + `Token efgh5678efgh5678… with id abcd123 name abcd001 created ${now.slice(0, 10)}`, '', - `Token hgfe8765… with id abcd125 created ${now.slice(0, 10)}`, + `Token hgfe8765… with id abcd125 name abcd002 created ${now.slice(0, 10)}`, 'with IP whitelist: 192.168.1.1/32', '', ]) @@ -104,9 +106,9 @@ t.test('token list parseable output', async t => { registry.getTokens(tokens) await npm.exec('token', []) t.strictSame(outputs, [ - 'key\ttoken\tcreated\treadonly\tCIDR whitelist', - `abcd1234abcd1234\tefgh5678efgh5678\t${now}\tfalse\t`, - `abcd1256\thgfe8765\t${now}\ttrue\t192.168.1.1/32`, + 'key\ttoken\tid\tname\tcreated\treadonly\tCIDR whitelist', + `abcd1234abcd1234\tefgh5678efgh5678\tabcd123\tabcd001\t${now}\tfalse\t`, + `abcd1256\thgfe8765\tabcd125\tabcd002\t${now}\ttrue\t192.168.1.1/32`, ]) }) diff --git a/deps/npm/test/lib/utils/allow-scripts-prune.js b/deps/npm/test/lib/utils/allow-scripts-prune.js new file mode 100644 index 00000000000000..880b1dfe343717 --- /dev/null +++ b/deps/npm/test/lib/utils/allow-scripts-prune.js @@ -0,0 +1,90 @@ +const t = require('tap') +const { classifyUnusedEntries } = require('../../../lib/utils/allow-scripts-prune.js') + +// Minimal registry node: `matches` derives name/version from the resolved URL. +const node = ({ name = 'pkg', version = '1.0.0' } = {}) => ({ + name, + version, + isRegistryDependency: true, + resolved: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`, +}) + +const withScripts = (overrides) => ({ node: node(overrides), hasScripts: true }) +const noScripts = (overrides) => ({ node: node(overrides), hasScripts: false }) + +t.test('empty / nullish policy', t => { + t.same(classifyUnusedEntries({}, []), { remaining: {}, removed: [] }) + t.same(classifyUnusedEntries(null, []), { remaining: {}, removed: [] }) + t.same(classifyUnusedEntries(undefined, [withScripts()]), { remaining: {}, removed: [] }) + t.end() +}) + +t.test('keeps entries that match an installed package with scripts', t => { + const { remaining, removed } = classifyUnusedEntries( + { canvas: true, 'esbuild@1.0.0': true }, + [withScripts({ name: 'canvas' }), withScripts({ name: 'esbuild', version: '1.0.0' })] + ) + t.same(remaining, { canvas: true, 'esbuild@1.0.0': true }) + t.same(removed, []) + t.end() +}) + +t.test('removes entries for packages no longer installed', t => { + const { remaining, removed } = classifyUnusedEntries( + { canvas: true, gone: true }, + [withScripts({ name: 'canvas' })] + ) + t.same(remaining, { canvas: true }) + t.same(removed, [{ key: 'gone', value: true, reason: 'not-installed' }]) + t.end() +}) + +t.test('removes entries whose package no longer has install scripts', t => { + const { remaining, removed } = classifyUnusedEntries( + { canvas: true }, + [noScripts({ name: 'canvas' })] + ) + t.same(remaining, {}) + t.same(removed, [{ key: 'canvas', value: true, reason: 'no-scripts' }]) + t.end() +}) + +t.test('a matching version with scripts keeps the entry even if another lacks them', t => { + const { remaining, removed } = classifyUnusedEntries( + { canvas: true }, + [noScripts({ name: 'canvas', version: '1.0.0' }), withScripts({ name: 'canvas', version: '2.0.0' })] + ) + t.same(remaining, { canvas: true }) + t.same(removed, []) + t.end() +}) + +t.test('version-pinned entry is unused when that exact version is not installed', t => { + const { remaining, removed } = classifyUnusedEntries( + { 'canvas@1.0.0': true }, + [withScripts({ name: 'canvas', version: '2.0.0' })] + ) + t.same(remaining, {}) + t.same(removed, [{ key: 'canvas@1.0.0', value: true, reason: 'not-installed' }]) + t.end() +}) + +t.test('prunes unused deny (false) entries the same way', t => { + const { remaining, removed } = classifyUnusedEntries( + { 'denied-gone': false, 'denied-here': false }, + [withScripts({ name: 'denied-here' })] + ) + t.same(remaining, { 'denied-here': false }) + t.same(removed, [{ key: 'denied-gone', value: false, reason: 'not-installed' }]) + t.end() +}) + +t.test('keeps unparseable keys untouched', t => { + const { remaining, removed } = classifyUnusedEntries( + { 'a b': true, gone: true }, + [withScripts({ name: 'canvas' })] + ) + t.same(remaining, { 'a b': true }) + t.same(removed, [{ key: 'gone', value: true, reason: 'not-installed' }]) + t.end() +}) diff --git a/deps/npm/test/lib/utils/allow-scripts-writer.js b/deps/npm/test/lib/utils/allow-scripts-writer.js index f72a84eef113ba..8edf25be3079c7 100644 --- a/deps/npm/test/lib/utils/allow-scripts-writer.js +++ b/deps/npm/test/lib/utils/allow-scripts-writer.js @@ -24,6 +24,23 @@ const node = (overrides = {}) => { } } +// A registry node with no `resolved` URL in the lockfile. Its trusted name +// comes from a dependency edge, but its version isn't trustable, so +// versionedKeyFor returns null (npm/cli#9558). +const noResolvedNode = (overrides = {}) => { + const name = overrides.name ?? 'pkg' + const version = overrides.version ?? '1.0.0' + return { + name, + packageName: overrides.packageName ?? name, + version, + resolved: undefined, + location: `node_modules/${name}`, + isRegistryDependency: true, + edgesIn: new Set([{ name, spec: overrides.spec ?? `^${version}` }]), + } +} + t.test('nameKeyFor / versionedKeyFor — registry', async t => { const n = node({ name: 'canvas', version: '2.11.0' }) t.equal(nameKeyFor(n), 'canvas') @@ -214,6 +231,73 @@ t.test('applyApprovalForPackage — keeps existing pin matching one installed, a t.strictSame(allowScripts, { 'lodash@3.10.1': true, 'lodash@4.17.21': true }) }) +t.test('versionedKeyFor — registry dep without a resolved URL has no trustable version', async t => { + const n = noResolvedNode({ name: 'esbuild', version: '0.25.0' }) + t.equal(nameKeyFor(n), 'esbuild', 'name still recoverable from the dependency edge') + t.equal(versionedKeyFor(n), null, 'version cannot be trusted without a resolved URL') +}) + +t.test('applyApprovalForPackage — pin mode falls back to name-only when version is not trustable', async t => { + const { allowScripts, changes, warning } = applyApprovalForPackage( + {}, + [noResolvedNode({ name: 'esbuild', version: '0.25.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { esbuild: true }, 'approved by name instead of silently skipped') + t.strictSame(changes, [{ key: 'esbuild', change: 'added' }]) + t.match(warning, /no "resolved" URL/, 'explains why a pin was not written') +}) + +t.test('applyApprovalForPackage — mixed resolved/no-resolved collapses to one name-only entry', async t => { + const { allowScripts, warning } = applyApprovalForPackage( + {}, + [ + node({ name: 'esbuild', version: '0.21.5' }), + noResolvedNode({ name: 'esbuild', version: '0.25.0' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { esbuild: true }, 'no redundant pin alongside the name-only entry') + t.match(warning, /no "resolved" URL/) +}) + +t.test('applyApprovalForPackage — existing pin collapses into name-only when a sibling cannot be pinned', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { 'esbuild@0.21.5': true }, + [ + node({ name: 'esbuild', version: '0.21.5' }), + noResolvedNode({ name: 'esbuild', version: '0.25.0' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { esbuild: true }) + t.match(changes, [ + { key: 'esbuild@0.21.5', change: 'removed-pinned-allow' }, + { key: 'esbuild', change: 'added' }, + ]) +}) + +t.test('applyApprovalForPackage — no-resolved dep already approved by name is a no-op', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { esbuild: true }, + [noResolvedNode({ name: 'esbuild', version: '0.25.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { esbuild: true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — no-resolved dep with name-only deny still loses to the deny', async t => { + const { allowScripts, changes, warning } = applyApprovalForPackage( + { esbuild: false }, + [noResolvedNode({ name: 'esbuild', version: '0.25.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { esbuild: false }, 'deny wins; nothing approved') + t.strictSame(changes, []) + t.match(warning, /esbuild is denied/) +}) + t.test('applyDenyForPackage — empty allowScripts adds name-only false', async t => { const { allowScripts, changes } = applyDenyForPackage( {}, @@ -620,7 +704,7 @@ t.test('denyWarning branches on key shape per RFC §approve-scripts', async t => { pin: true } ) t.match(pinned.warning, /versioned deny/) - t.match(pinned.warning, /npm deny-scripts canvas/) + t.match(pinned.warning, /npm install-scripts deny canvas/) t.match(pinned.warning, /widen the deny to all versions/) t.match(pinned.warning, /remove the entry/) @@ -631,7 +715,7 @@ t.test('denyWarning branches on key shape per RFC §approve-scripts', async t => { pin: true } ) t.match(multi.warning, /versioned deny/) - t.match(multi.warning, /npm deny-scripts canvas/) + t.match(multi.warning, /npm install-scripts deny canvas/) }) t.test('denyWarning: tag-type key (pkg@latest: false) is name-only', async t => { diff --git a/deps/npm/test/lib/utils/reify-output.js b/deps/npm/test/lib/utils/reify-output.js index 597a03374815ba..0f2be24b0246e2 100644 --- a/deps/npm/test/lib/utils/reify-output.js +++ b/deps/npm/test/lib/utils/reify-output.js @@ -391,6 +391,25 @@ t.test('added packages should be looked up within returned tree', async t => { t.matchSnapshot(out) }) + + t.test('linked store package counted though absent from actualTree', async t => { + const out = await mockReify(t, { + actualTree: { + name: 'foo', + inventory: { + has: () => false, + }, + }, + diff: { + children: [ + { action: 'ADD', ideal: { path: 'test/baz', name: 'baz', isInStore: true, isLink: false, package: { version: '1.0.0' } } }, + { action: 'ADD', ideal: { path: 'test/baz-link', name: 'baz', isInStore: false, isLink: true, package: { version: '1.0.0' } } }, + ], + }, + }) + + t.matchSnapshot(out) + }) }) t.test('prints dedupe difference on dry-run', async t => { @@ -484,7 +503,7 @@ t.test('prints unreviewed install scripts summary', async t => { 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.match(warn, /npm install-scripts ls/) }) t.test('global install suggests --allow-scripts, not approve-scripts', async t => { diff --git a/deps/npm/test/lib/utils/sbom-cyclonedx.js b/deps/npm/test/lib/utils/sbom-cyclonedx.js index ea569d41c57d8b..f3105e3cd48798 100644 --- a/deps/npm/test/lib/utils/sbom-cyclonedx.js +++ b/deps/npm/test/lib/utils/sbom-cyclonedx.js @@ -257,6 +257,17 @@ t.test('single node - from git url', t => { t.end() }) +t.test('git url with special chars is encoded into the vcs_url qualifier', t => { + const node = { ...root, type: 'git', resolved: 'https://github.com/foo/bar.git?a=b&c=d#1234' } + const res = cyclonedxOutput({ npm, nodes: [node] }) + const { purl } = res.metadata.component + // everything after vcs_url= must be a single percent-encoded value, so the + // committish/query can't leak out as an extra purl qualifier or subpath + t.equal(purl, 'pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar.git%3Fa%3Db%26c%3Dd%231234') + t.notMatch(purl.split('vcs_url=')[1], /[#&]/) + t.end() +}) + t.test('single node - no package info', t => { const node = { ...root, package: undefined } const res = cyclonedxOutput({ npm, nodes: [node] }) diff --git a/deps/npm/test/lib/utils/sbom-spdx.js b/deps/npm/test/lib/utils/sbom-spdx.js index d2599b0824510c..1e21c945ca75d5 100644 --- a/deps/npm/test/lib/utils/sbom-spdx.js +++ b/deps/npm/test/lib/utils/sbom-spdx.js @@ -223,6 +223,17 @@ t.test('single node - from git url', t => { t.end() }) +t.test('git url with special chars is encoded into the vcs_url qualifier', t => { + const node = { ...root, type: 'git', resolved: 'https://github.com/foo/bar.git?a=b&c=d#1234' } + const res = spdxOutput({ npm, nodes: [node] }) + const purl = res.packages + .find(p => p.SPDXID === 'SPDXRef-Package-root-1.0.0') + .externalRefs.find(r => r.referenceType === 'purl').referenceLocator + t.equal(purl, 'pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar.git%3Fa%3Db%26c%3Dd%231234') + t.notMatch(purl.split('vcs_url=')[1], /[#&]/) + t.end() +}) + t.test('single node - linked', t => { const node = { ...root, isLink: true, target: { edgesOut: [] } } const res = spdxOutput({ npm, nodes: [node] }) diff --git a/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js b/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js index e0c54105f88632..c67a6e4853a12d 100644 --- a/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js +++ b/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js @@ -76,6 +76,19 @@ t.test('throws when unreviewed install scripts exist (idealTree path)', async t ) }) +t.test('passes when the only unreviewed node is inert (platform-incompatible optional dep)', async t => { + // An inert dep is in the ideal tree but removed before any script runs, so + // strict mode must not reject it (npm/cli#9562). + const inertNode = { ...node({ name: 'fsevents' }), inert: true } + const arb = makeArb({ ideal: tree([inertNode]) }) + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('no error thrown for inert node') +}) + t.test('passes when all install-script nodes are explicitly approved', async t => { const arb = makeArb({ ideal: tree([node({ name: 'canvas' })]), @@ -190,7 +203,7 @@ t.test('error label falls back to node.name when package.version is missing', as ) }) -t.test('project-scoped error suggests approve-scripts / deny-scripts', async t => { +t.test('project-scoped error suggests install-scripts approve / deny', async t => { const arb = makeArb({ ideal: tree([node({ name: 'canvas' })]) }) await t.rejects( preflight({ @@ -198,7 +211,7 @@ t.test('project-scoped error suggests approve-scripts / deny-scripts', async t = npm: { flatOptions: { strictAllowScripts: true } }, idealTreeOpts: {}, }), - { message: /Approve them with `npm approve-scripts`/ } + { message: /Approve them with `npm install-scripts approve`/ } ) }) diff --git a/deps/v8/include/v8-template.h b/deps/v8/include/v8-template.h index 35c3c34e8badbc..299e17f86aa052 100644 --- a/deps/v8/include/v8-template.h +++ b/deps/v8/include/v8-template.h @@ -683,6 +683,13 @@ enum class PropertyHandlerFlags { */ kHasNoSideEffect = 1 << 2, + /** + * The interceptor may return non-configurable (PropertyAttribute::DontDelete) + * properties. When set on a global object's interceptor, it will be consulted + * during HasRestrictedGlobalProperty checks for lexical declarations. + */ + kHasDontDeleteProperty = 1 << 3, + /** * This flag is used to distinguish which callbacks were provided - * GenericNamedPropertyXXXCallback (old signature) or diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc index bdb9f715de95b4..fbd628370c0b1b 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -1613,6 +1613,8 @@ i::DirectHandle CreateInterceptorInfo( !(flags & PropertyHandlerFlags::kOnlyInterceptStrings)); obj->set_non_masking(flags & PropertyHandlerFlags::kNonMasking); obj->set_has_no_side_effect(flags & PropertyHandlerFlags::kHasNoSideEffect); + obj->set_has_dont_delete_property( + flags & PropertyHandlerFlags::kHasDontDeleteProperty); if (data.IsEmpty()) { data = v8::Undefined(reinterpret_cast(i_isolate)); diff --git a/deps/v8/src/execution/execution.cc b/deps/v8/src/execution/execution.cc index 37c313309e1e24..4dfd84f842e061 100644 --- a/deps/v8/src/execution/execution.cc +++ b/deps/v8/src/execution/execution.cc @@ -230,18 +230,14 @@ MaybeDirectHandle NewScriptContext( } if (IsLexicalVariableMode(mode)) { - LookupIterator lookup_it(isolate, global_object, name, global_object, - LookupIterator::OWN_SKIP_INTERCEPTOR); - Maybe maybe = - JSReceiver::GetPropertyAttributes(&lookup_it); - // Can't fail since the we looking up own properties on the global object - // skipping interceptors. - CHECK(!maybe.IsNothing()); - if ((maybe.FromJust() & DONT_DELETE) != 0) { - // ES#sec-globaldeclarationinstantiation 5.a: + Maybe has_restricted = JSGlobalObject::HasRestrictedGlobalProperty( + isolate, global_object, name); + if (has_restricted.IsNothing()) return MaybeDirectHandle(); + if (has_restricted.FromJust()) { + // https://tc39.es/ecma262/#sec-globaldeclarationinstantiation 3.a: // If envRec.HasVarDeclaration(name) is true, throw a SyntaxError // exception. - // ES#sec-globaldeclarationinstantiation 5.d: + // https://tc39.es/ecma262/#sec-globaldeclarationinstantiation 3.d: // If hasRestrictedGlobal is true, throw a SyntaxError exception. MessageLocation location(script, 0, 1); isolate->ThrowAt(isolate->factory()->NewSyntaxError( diff --git a/deps/v8/src/objects/api-callbacks-inl.h b/deps/v8/src/objects/api-callbacks-inl.h index f08e91f563cf2c..7c28fedd9a2c17 100644 --- a/deps/v8/src/objects/api-callbacks-inl.h +++ b/deps/v8/src/objects/api-callbacks-inl.h @@ -203,6 +203,8 @@ BOOL_ACCESSORS(InterceptorInfo, flags, has_no_side_effect, // TODO(ishell): remove once all the Api changes are done. BOOL_ACCESSORS(InterceptorInfo, flags, has_new_callbacks_signature, HasNewCallbacksSignatureBit::kShift) +BOOL_ACCESSORS(InterceptorInfo, flags, has_dont_delete_property, + HasDontDeletePropertyBit::kShift) void InterceptorInfo::RemoveCallbackRedirectionForSerialization( IsolateForSandbox isolate) { diff --git a/deps/v8/src/objects/api-callbacks.h b/deps/v8/src/objects/api-callbacks.h index f6c7c35a4dc7f6..f55cf0881555b6 100644 --- a/deps/v8/src/objects/api-callbacks.h +++ b/deps/v8/src/objects/api-callbacks.h @@ -182,6 +182,7 @@ class InterceptorInfo // TODO(ishell): remove support for old signatures once they go through // Api deprecation process. DECL_BOOLEAN_ACCESSORS(has_new_callbacks_signature) + DECL_BOOLEAN_ACCESSORS(has_dont_delete_property) DEFINE_TORQUE_GENERATED_INTERCEPTOR_INFO_FLAGS() diff --git a/deps/v8/src/objects/api-callbacks.tq b/deps/v8/src/objects/api-callbacks.tq index 6c4946e941c78e..bb5c3b9aaf54da 100644 --- a/deps/v8/src/objects/api-callbacks.tq +++ b/deps/v8/src/objects/api-callbacks.tq @@ -8,6 +8,7 @@ bitfield struct InterceptorInfoFlags extends uint31 { named: bool: 1 bit; has_no_side_effect: bool: 1 bit; has_new_callbacks_signature: bool: 1 bit; + has_dont_delete_property: bool: 1 bit; } extern class InterceptorInfo extends HeapObject { diff --git a/deps/v8/src/objects/js-objects.cc b/deps/v8/src/objects/js-objects.cc index 8e18b4fe6602db..9c1f619fd0d75e 100644 --- a/deps/v8/src/objects/js-objects.cc +++ b/deps/v8/src/objects/js-objects.cc @@ -5785,6 +5785,24 @@ void JSGlobalObject::InvalidatePropertyCell(DirectHandle global, value); } +// static +Maybe JSGlobalObject::HasRestrictedGlobalProperty( + Isolate* isolate, DirectHandle global, + DirectHandle name) { + LookupIterator::Configuration config = LookupIterator::OWN_SKIP_INTERCEPTOR; + if (global->HasNamedInterceptor() && + global->GetNamedInterceptor()->has_dont_delete_property()) { + config = LookupIterator::OWN; + } + LookupIterator it(isolate, global, name, global, config); + Maybe maybe = JSReceiver::GetPropertyAttributes(&it); + if (maybe.IsNothing()) return Nothing(); + // Global var and function bindings (except those that are introduced by + // non-strict direct eval) are non-configurable and are therefore restricted + // global properties. + return Just((maybe.FromJust() & DONT_DELETE) != 0); +} + // static MaybeDirectHandle JSDate::New(Isolate* isolate, DirectHandle constructor, diff --git a/deps/v8/src/objects/js-objects.h b/deps/v8/src/objects/js-objects.h index e289aae8aca06b..fa7c2ec4179abd 100644 --- a/deps/v8/src/objects/js-objects.h +++ b/deps/v8/src/objects/js-objects.h @@ -1218,6 +1218,11 @@ class JSGlobalObject static void InvalidatePropertyCell(DirectHandle object, DirectHandle name); + // https://tc39.es/ecma262/#sec-hasrestrictedglobalproperty + static Maybe HasRestrictedGlobalProperty( + Isolate* isolate, DirectHandle global, + DirectHandle name); + inline bool IsDetached(); inline Tagged native_context(); diff --git a/deps/v8/test/cctest/test-api-interceptors.cc b/deps/v8/test/cctest/test-api-interceptors.cc index 2867809be4091e..6e801321c2abb3 100644 --- a/deps/v8/test/cctest/test-api-interceptors.cc +++ b/deps/v8/test/cctest/test-api-interceptors.cc @@ -834,6 +834,49 @@ THREADED_TEST(InterceptorFunctionRedeclareWithQueryCallback) { v8::Script::Compile(ctx, code).ToLocalChecked()->Run(ctx).ToLocalChecked(); } +// A lexical declaration must throw when the interceptor reports the property +// as non-configurable (DontDelete), per HasRestrictedGlobalProperty checks in +// https://tc39.es/ecma262/#sec-globaldeclarationinstantiation. +THREADED_TEST(LexicalDeclThrowsForRestrictedGlobalViaInterceptor) { + v8::HandleScope scope(CcTest::isolate()); + LocalContext env; + v8::Local templ = + v8::FunctionTemplate::New(CcTest::isolate()); + + v8::Local object_template = templ->InstanceTemplate(); + object_template->SetHandler(v8::NamedPropertyHandlerConfiguration( + nullptr, nullptr, QueryCallbackSetDontDelete, nullptr, nullptr, + v8::Local(), + v8::PropertyHandlerFlags::kHasDontDeleteProperty)); + v8::Local ctx = + v8::Context::New(CcTest::isolate(), nullptr, object_template); + + v8::TryCatch try_catch(CcTest::isolate()); + v8::Local code = v8_str("let x;"); + CHECK(v8::Script::Compile(ctx, code).ToLocalChecked()->Run(ctx).IsEmpty()); + CHECK(try_catch.HasCaught()); +} + +// Without kHasDontDeleteProperty, the interceptor is not consulted for +// HasRestrictedGlobalProperty and lexical declarations succeed. +THREADED_TEST(LexicalDeclSucceedsWithoutRestrictedGlobalFlag) { + v8::HandleScope scope(CcTest::isolate()); + LocalContext env; + v8::Local templ = + v8::FunctionTemplate::New(CcTest::isolate()); + + v8::Local object_template = templ->InstanceTemplate(); + object_template->SetHandler(v8::NamedPropertyHandlerConfiguration( + nullptr, nullptr, QueryCallbackSetDontDelete)); + v8::Local ctx = + v8::Context::New(CcTest::isolate(), nullptr, object_template); + + v8::TryCatch try_catch(CcTest::isolate()); + v8::Local code = v8_str("let x;"); + CHECK(!v8::Script::Compile(ctx, code).ToLocalChecked()->Run(ctx).IsEmpty()); + CHECK(!try_catch.HasCaught()); +} + // Regression test for chromium bug 656648. // Do not crash on non-masking, intercepting setter callbacks. THREADED_TEST(NonMaskingInterceptor) { diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 463d56abe8626f..7e380f7425fbd5 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -596,7 +596,7 @@ UTF-8 string. ### `blob.textStream()` * Returns: {ReadableStream} diff --git a/doc/api/cli.md b/doc/api/cli.md index 4673d7bec4d678..d3c8d415326593 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1040,7 +1040,7 @@ added: - v23.6.0 - v22.20.0 changes: - - version: REPLACEME + - version: v26.5.0 pr-url: https://github.com/nodejs/node/pull/64221 description: This is enabled by default. --> @@ -1274,7 +1274,7 @@ Previously gated the entire `import.meta.resolve` feature. > Stability: 1.0 - Early development @@ -1361,7 +1361,7 @@ added: - v22.0.0 - v20.17.0 changes: - - version: REPLACEME + - version: v26.5.0 pr-url: https://github.com/nodejs/node/pull/64154 description: Print the top-level awaits without evaluating the modules. --> @@ -2011,14 +2011,6 @@ node --max-old-space-size-percentage=50 index.js node --max-old-space-size-percentage=75 index.js ``` -### `--napi-modules` - - - -This option is a no-op. It is kept for compatibility. - ### `--network-family-autoselection-attempt-timeout` diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 172af6bbc6edee..0623e876083b82 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -715,6 +715,25 @@ This is unsafe and dangerous. The returned pointer can become invalid if the underlying memory is detached, resized, transferred, or otherwise invalidated. Using stale pointers can cause memory corruption or process crashes. +## `ffi.getCurrentEventLoop()` + + + +* Returns: {bigint} + +Returns the address of the current thread's `uv_loop_t` as a `bigint`. + +The returned address is for the current Node.js environment. In the main thread, +this is the main thread event loop. In a worker thread, this is that worker's +event loop. + +This is unsafe and dangerous. The returned pointer is only valid for the lifetime +of the current environment. Using it after the environment exits, or from native +code that assumes a different thread or lifetime, can crash the process or +corrupt memory. + ## Safety notes The `node:ffi` module does not track pointer validity, memory ownership, or diff --git a/doc/api/http2.md b/doc/api/http2.md index 7511fe481942ca..1003fb3fb0e90e 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -4414,6 +4414,9 @@ added: v8.4.0 This method adds HTTP trailing headers (a header but at the end of the message) to the response. +Trailers must be added before calling [`response.end()`][]; trailers added +afterwards are silently dropped. + Attempting to set a header field name or value that contains invalid characters will result in a [`TypeError`][] being thrown. diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 60c42ab1a8b8ba..78a3b6210326bb 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1707,7 +1707,7 @@ are not guaranteed to reflect any correct state of the event loop. diff --git a/doc/api/test.md b/doc/api/test.md index 504dcf9a99336c..3b1ea94eff9781 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -3440,6 +3440,10 @@ added: - v18.9.0 - v16.19.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/64309 + description: Added `entryFile` to events forwarded from child processes + when tests run with process isolation. - version: v26.3.0 pr-url: https://github.com/nodejs/node/pull/63435 description: Added `parentId` to test events that carry a `testId`. @@ -3524,6 +3528,10 @@ Emitted when code coverage is enabled and all tests have completed. * `cause` {Error} The actual error thrown by the test. * `type` {string|undefined} The type of the test, used to denote whether this is a suite. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3553,6 +3561,10 @@ The corresponding declaration ordered events are `'test:pass'` and `'test:fail'` * `data` {Object} * `column` {number|undefined} The column number where the test is defined, or `undefined` if the test was run through the REPL. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3579,6 +3591,10 @@ defined. The corresponding declaration ordered event is `'test:start'`. * `data` {Object} * `column` {number|undefined} The column number where the test is defined, or `undefined` if the test was run through the REPL. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3600,6 +3616,10 @@ defined. * `data` {Object} * `column` {number|undefined} The column number where the test is defined, or `undefined` if the test was run through the REPL. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3632,6 +3652,10 @@ Emitted when a test is enqueued for execution. this is a suite. * `attempt` {number|undefined} The attempt number of the test run, present only when using the [`--test-rerun-failures`][] flag. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3697,6 +3721,10 @@ since the parent runner only knows about file-level tests. When using present only when using the [`--test-rerun-failures`][] flag. * `passed_on_attempt` {number|undefined} The attempt number the test passed on, present only when using the [`--test-rerun-failures`][] flag. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3726,6 +3754,10 @@ The corresponding execution ordered event is `'test:complete'`. * `data` {Object} * `column` {number|undefined} The column number where the test is defined, or `undefined` if the test was run through the REPL. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3742,6 +3774,10 @@ defined. * `data` {Object} * `column` {number|undefined} The column number where the test is defined, or `undefined` if the test was run through the REPL. + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. May differ from + `file` when the test is defined in a module imported by the entry file. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `line` {number|undefined} The line number where the test is defined, or @@ -3766,6 +3802,9 @@ The corresponding execution ordered event is `'test:dequeue'`. ### Event: `'test:stderr'` * `data` {Object} + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. * `file` {string} The path of the test file. * `message` {string} The message written to `stderr`. @@ -3777,6 +3816,9 @@ defined. ### Event: `'test:stdout'` * `data` {Object} + * `entryFile` {string|undefined} The path of the test file that was + executed as the entry point of the child process that emitted this event. + Only present when tests run with process isolation. * `file` {string} The path of the test file. * `message` {string} The message written to `stdout`. diff --git a/doc/api/webstreams.md b/doc/api/webstreams.md index 263106b5d9e74a..cf03323913b07a 100644 --- a/doc/api/webstreams.md +++ b/doc/api/webstreams.md @@ -109,7 +109,7 @@ For more details refer to the relevant documentation: ### `ReadableStreamTee(stream[, cloneForBranch2])` > Stability: 1 - Experimental diff --git a/doc/api/zlib.md b/doc/api/zlib.md index 3f32a7507b1fa0..f32715ba6cc6b2 100644 --- a/doc/api/zlib.md +++ b/doc/api/zlib.md @@ -801,7 +801,7 @@ These advanced options are available for controlling decompression: diff --git a/doc/changelogs/CHANGELOG_V26.md b/doc/changelogs/CHANGELOG_V26.md index 650f73bf8ae91f..82ec10df0cf598 100644 --- a/doc/changelogs/CHANGELOG_V26.md +++ b/doc/changelogs/CHANGELOG_V26.md @@ -8,6 +8,7 @@ +26.5.0
26.4.0
26.3.1
26.3.0
@@ -46,6 +47,157 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2026-07-08, Version 26.5.0 (Current), @richardlau + +### Notable Changes + +#### New release key + +Welcome to our newest releaser, [Stewart X Addison](https://github.com/sxa). Future Node.js releases may be signed with his [release key](https://github.com/nodejs/node/blob/main/README.md#release-keys), `655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD`. + +#### Other notable changes + +* \[[`55f48446c7`](https://github.com/nodejs/node/commit/55f48446c7)] - **(SEMVER-MINOR)** **buffer**: implement blob.textStream() (Matthew Aitken) [#64036](https://github.com/nodejs/node/pull/64036) +* \[[`b373202efc`](https://github.com/nodejs/node/commit/b373202efc)] - **(SEMVER-MINOR)** **esm**: add `--experimental-import-text` flag (Efe) [#62300](https://github.com/nodejs/node/pull/62300) +* \[[`39e0c14455`](https://github.com/nodejs/node/commit/39e0c14455)] - **(SEMVER-MINOR)** **perf\_hooks**: sample delay per event loop iteration (Pablo Erhard) [#62935](https://github.com/nodejs/node/pull/62935) +* \[[`999a83c937`](https://github.com/nodejs/node/commit/999a83c937)] - **(SEMVER-MINOR)** **stream**: expose ReadableStreamTee (Matteo Collina) [#64195](https://github.com/nodejs/node/pull/64195) +* \[[`4e0236dc3d`](https://github.com/nodejs/node/commit/4e0236dc3d)] - **(SEMVER-MINOR)** **tls**: report negotiated TLS groups (Filip Skokan) [#64119](https://github.com/nodejs/node/pull/64119) + +### Commits + +* \[[`87648c0a6c`](https://github.com/nodejs/node/commit/87648c0a6c)] - **benchmark**: trim down the argon2 sets (Filip Skokan) [#64218](https://github.com/nodejs/node/pull/64218) +* \[[`a483bfd3f0`](https://github.com/nodejs/node/commit/a483bfd3f0)] - **buffer**: remove unreachable overflow check in atob (haramjeong) [#60161](https://github.com/nodejs/node/pull/60161) +* \[[`6d14279688`](https://github.com/nodejs/node/commit/6d14279688)] - **buffer**: add fast api for isUtf8 and isAscii (Gürgün Dayıoğlu) [#64169](https://github.com/nodejs/node/pull/64169) +* \[[`55f48446c7`](https://github.com/nodejs/node/commit/55f48446c7)] - **(SEMVER-MINOR)** **buffer**: implement blob.textStream() (Matthew Aitken) [#64036](https://github.com/nodejs/node/pull/64036) +* \[[`a67d9a7a44`](https://github.com/nodejs/node/commit/a67d9a7a44)] - **build**: allow linting node.1 (Aviv Keller) [#64157](https://github.com/nodejs/node/pull/64157) +* \[[`06c1fbc25b`](https://github.com/nodejs/node/commit/06c1fbc25b)] - **build**: enable Maglev for riscv64 (Jamie Magee) [#62605](https://github.com/nodejs/node/pull/62605) +* \[[`518309c363`](https://github.com/nodejs/node/commit/518309c363)] - **build**: suppress clang errors building libffi on Windows (René) [#64222](https://github.com/nodejs/node/pull/64222) +* \[[`6a80ab485c`](https://github.com/nodejs/node/commit/6a80ab485c)] - **build**: add manually-dispatched stress-test workflow (Joyee Cheung) [#64118](https://github.com/nodejs/node/pull/64118) +* \[[`f4e7bf1f1c`](https://github.com/nodejs/node/commit/f4e7bf1f1c)] - **build**: pin envinfo versions in github actions (Joyee Cheung) [#64117](https://github.com/nodejs/node/pull/64117) +* \[[`66f6ac0d86`](https://github.com/nodejs/node/commit/66f6ac0d86)] - **build**: support setting an emulator from configure script (Ivan Trubach) [#53899](https://github.com/nodejs/node/pull/53899) +* \[[`7f26c54aa6`](https://github.com/nodejs/node/commit/7f26c54aa6)] - **child\_process**: fix permission model propagation via NODE\_OPTIONS (Matteo Collina) [#63972](https://github.com/nodejs/node/pull/63972) +* \[[`32bb554f5b`](https://github.com/nodejs/node/commit/32bb554f5b)] - **crypto**: fix large DH generator validation (Tobias Nießen) [#64092](https://github.com/nodejs/node/pull/64092) +* \[[`0908d76ef6`](https://github.com/nodejs/node/commit/0908d76ef6)] - **crypto**: reject small-order EdDSA points during verify (Filip Skokan) [#64026](https://github.com/nodejs/node/pull/64026) +* \[[`7f7e5863c2`](https://github.com/nodejs/node/commit/7f7e5863c2)] - **deps**: update undici to 8.7.0 (Node.js GitHub Bot) [#64282](https://github.com/nodejs/node/pull/64282) +* \[[`af91029801`](https://github.com/nodejs/node/commit/af91029801)] - **deps**: update nghttp3 to 1.17.0 (Node.js GitHub Bot) [#64182](https://github.com/nodejs/node/pull/64182) +* \[[`2e500ba7b0`](https://github.com/nodejs/node/commit/2e500ba7b0)] - **deps**: update googletest to 8b53336594cc52213c6c2c7a0b29194fa896d039 (Node.js GitHub Bot) [#64181](https://github.com/nodejs/node/pull/64181) +* \[[`74e3aa24ba`](https://github.com/nodejs/node/commit/74e3aa24ba)] - **deps**: update sqlite to 3.53.3 (Node.js GitHub Bot) [#64180](https://github.com/nodejs/node/pull/64180) +* \[[`c7e57f55a7`](https://github.com/nodejs/node/commit/c7e57f55a7)] - **deps**: c-ares: cherry-pick 8ba37af8e3fb (René) [#64110](https://github.com/nodejs/node/pull/64110) +* \[[`879fdc4daf`](https://github.com/nodejs/node/commit/879fdc4daf)] - **deps**: V8: backport da20a197a7f9 (Kevin Gibbons) [#64101](https://github.com/nodejs/node/pull/64101) +* \[[`a640543a7c`](https://github.com/nodejs/node/commit/a640543a7c)] - **deps**: V8: cherry-pick 0cc9eb22c0b0 (Kevin Gibbons) [#64101](https://github.com/nodejs/node/pull/64101) +* \[[`feefd179e5`](https://github.com/nodejs/node/commit/feefd179e5)] - **deps**: V8: cherry-pick 1a391f98cc7a (Kevin Gibbons) [#64101](https://github.com/nodejs/node/pull/64101) +* \[[`8ef643d4b0`](https://github.com/nodejs/node/commit/8ef643d4b0)] - **deps**: update googletest to 0b1e895ba4226c2fda5ee0178c9b5b1195a741aa (Node.js GitHub Bot) [#64039](https://github.com/nodejs/node/pull/64039) +* \[[`9e50bb0655`](https://github.com/nodejs/node/commit/9e50bb0655)] - **dgram**: skip dns.lookup() for literal IP addresses (Ruben Bridgewater) [#64133](https://github.com/nodejs/node/pull/64133) +* \[[`dc052c095c`](https://github.com/nodejs/node/commit/dc052c095c)] - **diagnostics\_channel**: return original thenable (Stephen Belanger) [#62407](https://github.com/nodejs/node/pull/62407) +* \[[`a22a840293`](https://github.com/nodejs/node/commit/a22a840293)] - **doc**: clarify QUIC stream state wording (EduardF1) [#63660](https://github.com/nodejs/node/pull/63660) +* \[[`8d4bec2d71`](https://github.com/nodejs/node/commit/8d4bec2d71)] - **doc**: update Http2SecureServer.on("timeout") default value (YuSheng Chen) [#64187](https://github.com/nodejs/node/pull/64187) +* \[[`da88f70afa`](https://github.com/nodejs/node/commit/da88f70afa)] - **doc**: add note on visibility of CI failures to new contributor guide (Stewart X Addison) [#64256](https://github.com/nodejs/node/pull/64256) +* \[[`20ce359ccb`](https://github.com/nodejs/node/commit/20ce359ccb)] - **doc**: clarify HTTP/1.1 response ordering (Matteo Collina) [#64213](https://github.com/nodejs/node/pull/64213) +* \[[`05eae2835c`](https://github.com/nodejs/node/commit/05eae2835c)] - **doc**: recommend node-stress-single-test for flaky tests (Trivikram Kamat) [#64223](https://github.com/nodejs/node/pull/64223) +* \[[`3966eb67e7`](https://github.com/nodejs/node/commit/3966eb67e7)] - **doc**: fix typo in examples (Vas Sudanagunta) [#64184](https://github.com/nodejs/node/pull/64184) +* \[[`12a2b9daa3`](https://github.com/nodejs/node/commit/12a2b9daa3)] - **doc**: fix typo in node-config-schema.json (Hamid Reza Ghavami) [#64188](https://github.com/nodejs/node/pull/64188) +* \[[`0854482671`](https://github.com/nodejs/node/commit/0854482671)] - **doc**: clarify defense-in-depth issues (Matteo Collina) [#64215](https://github.com/nodejs/node/pull/64215) +* \[[`ef4915fc3a`](https://github.com/nodejs/node/commit/ef4915fc3a)] - **doc**: fix Fast FFI argument count in ffi.md (Daijiro Wachi) [#63960](https://github.com/nodejs/node/pull/63960) +* \[[`bb2eed863c`](https://github.com/nodejs/node/commit/bb2eed863c)] - **doc**: add sxa GPG key (ed25519) (Stewart X Addison) [#64193](https://github.com/nodejs/node/pull/64193) +* \[[`b7bf6e3a06`](https://github.com/nodejs/node/commit/b7bf6e3a06)] - **doc**: add guide and answers to FAQs for first-time contributors (Joyee Cheung) [#63685](https://github.com/nodejs/node/pull/63685) +* \[[`ff537ba858`](https://github.com/nodejs/node/commit/ff537ba858)] - **doc**: update `Http2Server.close` & `Http2SecureServer.close` (YuSheng Chen) [#63298](https://github.com/nodejs/node/pull/63298) +* \[[`f3db304588`](https://github.com/nodejs/node/commit/f3db304588)] - **doc**: update list of people in `SECURITY.md` (Richard Lau) [#64152](https://github.com/nodejs/node/pull/64152) +* \[[`2a126647b0`](https://github.com/nodejs/node/commit/2a126647b0)] - **doc**: clarify vfs is not a sandbox (Matteo Collina) [#64143](https://github.com/nodejs/node/pull/64143) +* \[[`85fc79dd9b`](https://github.com/nodejs/node/commit/85fc79dd9b)] - **doc**: fix broken links and duplicate stability label (Antoine du Hamel) [#64130](https://github.com/nodejs/node/pull/64130) +* \[[`189e830eb3`](https://github.com/nodejs/node/commit/189e830eb3)] - **doc**: add missing option to man page (Richard Lau) [#64156](https://github.com/nodejs/node/pull/64156) +* \[[`7a16ccccd0`](https://github.com/nodejs/node/commit/7a16ccccd0)] - **doc**: announce upcoming end of tier 2 support for macOS x64 (Antoine du Hamel) [#63931](https://github.com/nodejs/node/pull/63931) +* \[[`d5f826045f`](https://github.com/nodejs/node/commit/d5f826045f)] - **doc**: update toolchain for official AIX releases (Richard Lau) [#64068](https://github.com/nodejs/node/pull/64068) +* \[[`60abc4400f`](https://github.com/nodejs/node/commit/60abc4400f)] - **doc**: fix callback example import in fs docs (Kamal Rawal) [#63912](https://github.com/nodejs/node/pull/63912) +* \[[`e470c74a6c`](https://github.com/nodejs/node/commit/e470c74a6c)] - **doc**: fix keepAliveTimeout default in http.createServer options (Jahanzaib iqbal) [#63974](https://github.com/nodejs/node/pull/63974) +* \[[`851b460583`](https://github.com/nodejs/node/commit/851b460583)] - **esm**: improve ERR\_REQUIRE\_ASYNC\_MODULE (Joyee Cheung) [#64260](https://github.com/nodejs/node/pull/64260) +* \[[`0cd443df39`](https://github.com/nodejs/node/commit/0cd443df39)] - **esm**: print required top-level await locations without evaluating (Joyee Cheung) [#64154](https://github.com/nodejs/node/pull/64154) +* \[[`b373202efc`](https://github.com/nodejs/node/commit/b373202efc)] - **(SEMVER-MINOR)** **esm**: add `--experimental-import-text` flag (Efe) [#62300](https://github.com/nodejs/node/pull/62300) +* \[[`eacfbd0ca5`](https://github.com/nodejs/node/commit/eacfbd0ca5)] - **http**: add CONNECT method handling for default Host header with proxy (Archkon) [#64114](https://github.com/nodejs/node/pull/64114) +* \[[`aeb539a383`](https://github.com/nodejs/node/commit/aeb539a383)] - **http**: fix drain event with cork/uncork (David Evans) [#64038](https://github.com/nodejs/node/pull/64038) +* \[[`8e8874b216`](https://github.com/nodejs/node/commit/8e8874b216)] - **http**: document and validate options.path when it's in absolute-form (Joyee Cheung) [#64108](https://github.com/nodejs/node/pull/64108) +* \[[`eb2e96bc28`](https://github.com/nodejs/node/commit/eb2e96bc28)] - **inspector**: fix crash when writing to closed inspector socket (ympark2011) [#64209](https://github.com/nodejs/node/pull/64209) +* \[[`243b0e4e57`](https://github.com/nodejs/node/commit/243b0e4e57)] - **lib**: reject string "0" in validatePort when allowZero is false (Daijiro Wachi) [#64174](https://github.com/nodejs/node/pull/64174) +* \[[`34a537c0ed`](https://github.com/nodejs/node/commit/34a537c0ed)] - **lib**: use `__proto__: null` when calling `ObjectDefineProperty` (Antoine du Hamel) [#64239](https://github.com/nodejs/node/pull/64239) +* \[[`1f72393f19`](https://github.com/nodejs/node/commit/1f72393f19)] - **lib**: lazily initialize kEvents and kHandlers maps (Guilherme Araújo) [#63702](https://github.com/nodejs/node/pull/63702) +* \[[`92a3dc3191`](https://github.com/nodejs/node/commit/92a3dc3191)] - **lib,permission**: fix addon permission drop (Martin Wagner) [#64007](https://github.com/nodejs/node/pull/64007) +* \[[`87b8f2a296`](https://github.com/nodejs/node/commit/87b8f2a296)] - **meta**: fix linter warning in `stale.yml` (Antoine du Hamel) [#64281](https://github.com/nodejs/node/pull/64281) +* \[[`829c4a5913`](https://github.com/nodejs/node/commit/829c4a5913)] - **meta**: bump actions/cache from 5.0.5 to 6.1.0 (dependabot\[bot]) [#64248](https://github.com/nodejs/node/pull/64248) +* \[[`0808dcd31c`](https://github.com/nodejs/node/commit/0808dcd31c)] - **meta**: bump github/codeql-action/autobuild from 4.36.1 to 4.36.2 (dependabot\[bot]) [#64247](https://github.com/nodejs/node/pull/64247) +* \[[`64aa17058f`](https://github.com/nodejs/node/commit/64aa17058f)] - **meta**: bump github/codeql-action/analyze from 4.36.1 to 4.36.2 (dependabot\[bot]) [#64246](https://github.com/nodejs/node/pull/64246) +* \[[`873d1e0412`](https://github.com/nodejs/node/commit/873d1e0412)] - **meta**: bump actions/checkout from 6.0.2 to 7.0.0 (dependabot\[bot]) [#64245](https://github.com/nodejs/node/pull/64245) +* \[[`fe460ccf0b`](https://github.com/nodejs/node/commit/fe460ccf0b)] - **meta**: bump codecov/codecov-action from 6.0.1 to 7.0.0 (dependabot\[bot]) [#64244](https://github.com/nodejs/node/pull/64244) +* \[[`845c63ed50`](https://github.com/nodejs/node/commit/845c63ed50)] - **meta**: bump rtCamp/action-slack-notify from 2.3.3 to 2.4.0 (dependabot\[bot]) [#64243](https://github.com/nodejs/node/pull/64243) +* \[[`2cad2d6de5`](https://github.com/nodejs/node/commit/2cad2d6de5)] - **meta**: bump github/codeql-action/init from 4.36.1 to 4.36.2 (dependabot\[bot]) [#64242](https://github.com/nodejs/node/pull/64242) +* \[[`0ddde950c7`](https://github.com/nodejs/node/commit/0ddde950c7)] - **meta**: bump actions/setup-python from 6.2.0 to 6.3.0 (dependabot\[bot]) [#64241](https://github.com/nodejs/node/pull/64241) +* \[[`c0a8760d2f`](https://github.com/nodejs/node/commit/c0a8760d2f)] - **meta**: bump github/codeql-action/upload-sarif from 4.36.1 to 4.36.2 (dependabot\[bot]) [#64240](https://github.com/nodejs/node/pull/64240) +* \[[`f49704b9d0`](https://github.com/nodejs/node/commit/f49704b9d0)] - **meta**: clarify V8 flags are outside threat model (Matteo Collina) [#64224](https://github.com/nodejs/node/pull/64224) +* \[[`6b8dc58e6e`](https://github.com/nodejs/node/commit/6b8dc58e6e)] - **meta**: move one or more collaborators to emeritus (Node.js GitHub Bot) [#64057](https://github.com/nodejs/node/pull/64057) +* \[[`fe5260cca7`](https://github.com/nodejs/node/commit/fe5260cca7)] - **meta**: update status of past strategic initiatives (Joyee Cheung) [#63480](https://github.com/nodejs/node/pull/63480) +* \[[`7b01040008`](https://github.com/nodejs/node/commit/7b01040008)] - **meta**: speed up stale bot (Aviv Keller) [#64075](https://github.com/nodejs/node/pull/64075) +* \[[`874c46c24f`](https://github.com/nodejs/node/commit/874c46c24f)] - **meta**: update sccache version in test-linux-quic (René) [#64043](https://github.com/nodejs/node/pull/64043) +* \[[`48c5c86363`](https://github.com/nodejs/node/commit/48c5c86363)] - **module**: enable import support for addons by default (Chengzhong Wu) [#64221](https://github.com/nodejs/node/pull/64221) +* \[[`39e0c14455`](https://github.com/nodejs/node/commit/39e0c14455)] - **(SEMVER-MINOR)** **perf\_hooks**: sample delay per event loop iteration (Pablo Erhard) [#62935](https://github.com/nodejs/node/pull/62935) +* \[[`f90f1bd032`](https://github.com/nodejs/node/commit/f90f1bd032)] - **perf\_hooks**: add NODE\_PERFORMANCE\_GC\_MINOR\_MARK\_SWEEP constant (Attila Szegedi) [#63877](https://github.com/nodejs/node/pull/63877) +* \[[`bdf32628c7`](https://github.com/nodejs/node/commit/bdf32628c7)] - **process**: fix finalization cleanup ref tracking (Trivikram Kamat) [#64087](https://github.com/nodejs/node/pull/64087) +* \[[`9a65b7fff4`](https://github.com/nodejs/node/commit/9a65b7fff4)] - **quic**: drop version negotiation packets with oversized CIDs (Mohamed Sayed) [#64228](https://github.com/nodejs/node/pull/64228) +* \[[`2699fe4706`](https://github.com/nodejs/node/commit/2699fe4706)] - **quic**: fixes undefined handle in QuicStream kInspect (Marten Richter) [#64170](https://github.com/nodejs/node/pull/64170) +* \[[`00dea28bb3`](https://github.com/nodejs/node/commit/00dea28bb3)] - **repl**: lazy-load acorn and defer vm context creation (Daijiro Wachi) [#63879](https://github.com/nodejs/node/pull/63879) +* \[[`ce659a1cf9`](https://github.com/nodejs/node/commit/ce659a1cf9)] - **src**: fix escaping of single quotes in task runner (Antoine du Hamel) [#64089](https://github.com/nodejs/node/pull/64089) +* \[[`dbb3126e5c`](https://github.com/nodejs/node/commit/dbb3126e5c)] - **src**: abstract tracing agent for both legacy and perfetto (Chengzhong Wu) [#64053](https://github.com/nodejs/node/pull/64053) +* \[[`12edf1d68d`](https://github.com/nodejs/node/commit/12edf1d68d)] - **src**: avoid redundant call to `std::get_if<>()` (Tobias Nießen) [#64094](https://github.com/nodejs/node/pull/64094) +* \[[`eda91b6d01`](https://github.com/nodejs/node/commit/eda91b6d01)] - **src**: avoid copying source string in TextEncoder.encode (Yagiz Nizipli) [#63897](https://github.com/nodejs/node/pull/63897) +* \[[`efbbb9a03c`](https://github.com/nodejs/node/commit/efbbb9a03c)] - **stream**: preserve half-open duplexes in async iteration (Efe) [#64275](https://github.com/nodejs/node/pull/64275) +* \[[`999a83c937`](https://github.com/nodejs/node/commit/999a83c937)] - **(SEMVER-MINOR)** **stream**: expose ReadableStreamTee (Matteo Collina) [#64195](https://github.com/nodejs/node/pull/64195) +* \[[`ab5ed72903`](https://github.com/nodejs/node/commit/ab5ed72903)] - **stream**: reject iter consumers on abort (Trivikram Kamat) [#64066](https://github.com/nodejs/node/pull/64066) +* \[[`d3fa77c5e2`](https://github.com/nodejs/node/commit/d3fa77c5e2)] - **stream**: fix merge abort for pending sources (Trivikram Kamat) [#64013](https://github.com/nodejs/node/pull/64013) +* \[[`38b99140ed`](https://github.com/nodejs/node/commit/38b99140ed)] - **stream**: refactor unnecessary optional chaining away (Antoine du Hamel) [#64253](https://github.com/nodejs/node/pull/64253) +* \[[`c81f894ebe`](https://github.com/nodejs/node/commit/c81f894ebe)] - **stream**: cut per-chunk overhead in WHATWG streams (Matteo Collina) [#64252](https://github.com/nodejs/node/pull/64252) +* \[[`f162234f24`](https://github.com/nodejs/node/commit/f162234f24)] - **stream**: normalize Broadcast.from() byte inputs (Trivikram Kamat) [#64082](https://github.com/nodejs/node/pull/64082) +* \[[`1182ad8f3b`](https://github.com/nodejs/node/commit/1182ad8f3b)] - **stream**: proxy first own method in Readable.wrap() (Daijiro Wachi) [#64048](https://github.com/nodejs/node/pull/64048) +* \[[`d0b830b382`](https://github.com/nodejs/node/commit/d0b830b382)] - **stream**: observe abort while awaiting pipeTo source (Trivikram Kamat) [#64015](https://github.com/nodejs/node/pull/64015) +* \[[`f7adcd8359`](https://github.com/nodejs/node/commit/f7adcd8359)] - **stream**: respect iter consumer abort signals (Trivikram Kamat) [#63997](https://github.com/nodejs/node/pull/63997) +* \[[`b09e624c6f`](https://github.com/nodejs/node/commit/b09e624c6f)] - **test**: make blob desiredSize assertion robust (Trivikram Kamat) [#64106](https://github.com/nodejs/node/pull/64106) +* \[[`d0d8f0c774`](https://github.com/nodejs/node/commit/d0d8f0c774)] - **test**: update WPT for urlpattern to 11a459a2b1 (Node.js GitHub Bot) [#64037](https://github.com/nodejs/node/pull/64037) +* \[[`ff9122c20c`](https://github.com/nodejs/node/commit/ff9122c20c)] - **test**: improve lcov reporter snapshot diagnostics (Trivikram Kamat) [#64049](https://github.com/nodejs/node/pull/64049) +* \[[`570952d4f3`](https://github.com/nodejs/node/commit/570952d4f3)] - **test**: keep finalization close fixture ref alive (Trivikram Kamat) [#64085](https://github.com/nodejs/node/pull/64085) +* \[[`1b4f213380`](https://github.com/nodejs/node/commit/1b4f213380)] - **test**: fix typo from overriden to overridden (parkhojeong) [#63403](https://github.com/nodejs/node/pull/63403) +* \[[`4c91090b8b`](https://github.com/nodejs/node/commit/4c91090b8b)] - **test**: fix typo from funciton to function (parkhojeong) [#63403](https://github.com/nodejs/node/pull/63403) +* \[[`bf080c7917`](https://github.com/nodejs/node/commit/bf080c7917)] - **test**: mark hr-time WPT flaky on macos15-x64 (Trivikram Kamat) [#64054](https://github.com/nodejs/node/pull/64054) +* \[[`24e32098c5`](https://github.com/nodejs/node/commit/24e32098c5)] - **test**: use one-off agent in http consumed timeout test (Trivikram Kamat) [#64052](https://github.com/nodejs/node/pull/64052) +* \[[`3229886de2`](https://github.com/nodejs/node/commit/3229886de2)] - **test**: fix flaky test-runner coverage threshold test (Trivikram Kamat) [#64051](https://github.com/nodejs/node/pull/64051) +* \[[`83b91ea6ec`](https://github.com/nodejs/node/commit/83b91ea6ec)] - **test\_runner**: filter execArgv fallback for child tests (Trivikram Kamat) [#64056](https://github.com/nodejs/node/pull/64056) +* \[[`269b609a3d`](https://github.com/nodejs/node/commit/269b609a3d)] - **test\_runner**: improve coverage failure diagnostics (Trivikram Kamat) [#64050](https://github.com/nodejs/node/pull/64050) +* \[[`0342744c34`](https://github.com/nodejs/node/commit/0342744c34)] - **test\_runner**: add timestamp to JUnit reporter testsuites (sangwook) [#64029](https://github.com/nodejs/node/pull/64029) +* \[[`086741d121`](https://github.com/nodejs/node/commit/086741d121)] - **timers**: reuse Timeout objects in setStreamTimeout (Matteo Collina) [#64254](https://github.com/nodejs/node/pull/64254) +* \[[`4e0236dc3d`](https://github.com/nodejs/node/commit/4e0236dc3d)] - **(SEMVER-MINOR)** **tls**: report negotiated TLS groups (Filip Skokan) [#64119](https://github.com/nodejs/node/pull/64119) +* \[[`3bdd7e20be`](https://github.com/nodejs/node/commit/3bdd7e20be)] - **tls**: handle large RSA exponents in X.509 cert (Tobias Nießen) [#64093](https://github.com/nodejs/node/pull/64093) +* \[[`c96c838977`](https://github.com/nodejs/node/commit/c96c838977)] - **tools**: update RUSTC\_VERSION for remaining GHA workflows (René) [#64325](https://github.com/nodejs/node/pull/64325) +* \[[`ee873b7aaf`](https://github.com/nodejs/node/commit/ee873b7aaf)] - **tools**: bump `temporal_rs` version (Antoine du Hamel) [#63281](https://github.com/nodejs/node/pull/63281) +* \[[`ea3b870155`](https://github.com/nodejs/node/commit/ea3b870155)] - **tools**: remove `envinfo` from our workflows (Antoine du Hamel) [#64259](https://github.com/nodejs/node/pull/64259) +* \[[`d940f02e8b`](https://github.com/nodejs/node/commit/d940f02e8b)] - **tools**: bump the eslint group in /tools/eslint with 8 updates (dependabot\[bot]) [#64249](https://github.com/nodejs/node/pull/64249) +* \[[`fe0ea2bb5d`](https://github.com/nodejs/node/commit/fe0ea2bb5d)] - **tools**: bump @node-core/doc-kit (dependabot\[bot]) [#64010](https://github.com/nodejs/node/pull/64010) +* \[[`4dceefde1e`](https://github.com/nodejs/node/commit/4dceefde1e)] - **tools**: bump undici from 6.24.1 to 6.27.0 in /tools/doc (dependabot\[bot]) [#64031](https://github.com/nodejs/node/pull/64031) +* \[[`6e187db7d7`](https://github.com/nodejs/node/commit/6e187db7d7)] - **tools**: update c-ares updater script (Antoine du Hamel) [#64194](https://github.com/nodejs/node/pull/64194) +* \[[`657a35f5a2`](https://github.com/nodejs/node/commit/657a35f5a2)] - **tools**: validate version number in release proposal commit message lint (Antoine du Hamel) [#64070](https://github.com/nodejs/node/pull/64070) +* \[[`17228a861c`](https://github.com/nodejs/node/commit/17228a861c)] - **tools**: add GHA benchmark runner (Antoine du Hamel) [#60293](https://github.com/nodejs/node/pull/60293) +* \[[`6d11a71d91`](https://github.com/nodejs/node/commit/6d11a71d91)] - **tools**: update `build-shared/action.yml` to a reusable workflow (Antoine du Hamel) [#64059](https://github.com/nodejs/node/pull/64059) +* \[[`7a17c50b7f`](https://github.com/nodejs/node/commit/7a17c50b7f)] - **tools**: update libffi updater script (Antoine du Hamel) [#64046](https://github.com/nodejs/node/pull/64046) +* \[[`28047a3e71`](https://github.com/nodejs/node/commit/28047a3e71)] - **tools**: exclude `libffi` changes from `test-shared` GHA CI (Antoine du Hamel) [#64047](https://github.com/nodejs/node/pull/64047) +* \[[`58d9685acc`](https://github.com/nodejs/node/commit/58d9685acc)] - **typings**: add typing for crypto (Filip Skokan) [#64122](https://github.com/nodejs/node/pull/64122) +* \[[`7a9dcad44d`](https://github.com/nodejs/node/commit/7a9dcad44d)] - **util**: fix OOM in inspect color stack formatting (Ijtihed Kilani) [#64022](https://github.com/nodejs/node/pull/64022) +* \[[`d5f01bbbde`](https://github.com/nodejs/node/commit/d5f01bbbde)] - **vfs**: reject rename into descendant directory (Trivikram Kamat) [#64285](https://github.com/nodejs/node/pull/64285) +* \[[`0b6af91081`](https://github.com/nodejs/node/commit/0b6af91081)] - **vfs**: handle current-position sentinel in memory files (Trivikram Kamat) [#64163](https://github.com/nodejs/node/pull/64163) +* \[[`322230d641`](https://github.com/nodejs/node/commit/322230d641)] - **vfs**: support writeFileSync with virtual fds (Trivikram Kamat) [#64165](https://github.com/nodejs/node/pull/64165) +* \[[`9395d209c7`](https://github.com/nodejs/node/commit/9395d209c7)] - **vfs**: avoid recursive readdir symlink cycles (Matteo Collina) [#64168](https://github.com/nodejs/node/pull/64168) +* \[[`bbdd7643b6`](https://github.com/nodejs/node/commit/bbdd7643b6)] - **vfs**: read RealFSProvider files from open fd (Trivikram Kamat) [#64104](https://github.com/nodejs/node/pull/64104) +* \[[`92859b8097`](https://github.com/nodejs/node/commit/92859b8097)] - **vm**: fix copying PropertyDescriptor (Chengzhong Wu) [#64073](https://github.com/nodejs/node/pull/64073) +* \[[`9046035475`](https://github.com/nodejs/node/commit/9046035475)] - **zlib**: validate flush king for all streams (Ic3b3rg) [#63746](https://github.com/nodejs/node/pull/63746) +* \[[`98be4304a3`](https://github.com/nodejs/node/commit/98be4304a3)] - **zlib**: validate flush kind for brotli streams (Ic3b3rg) [#63746](https://github.com/nodejs/node/pull/63746) +* \[[`90007a59a9`](https://github.com/nodejs/node/commit/90007a59a9)] - **zlib**: expose rejectGarbageAfterEnd option (Filip Skokan) [#64023](https://github.com/nodejs/node/pull/64023) +* \[[`5933516066`](https://github.com/nodejs/node/commit/5933516066)] - **zlib**: reject trailing gzip members in web streams (Filip Skokan) [#64023](https://github.com/nodejs/node/pull/64023) + ## 2026-06-24, Version 26.4.0 (Current), @aduh95 diff --git a/doc/contributing/pull-requests.md b/doc/contributing/pull-requests.md index 0893460cbc8b0d..7215c2440a8441 100644 --- a/doc/contributing/pull-requests.md +++ b/doc/contributing/pull-requests.md @@ -540,6 +540,13 @@ specific details of how to do this are included in the new collaborator test run for you as approvals for the pull request come in. If not, you can ask a collaborator or triager to start a CI run. +CI access is only available to collaborators and members of the platform +teams. If you are not yet in one of those teams then you will need someone +to relay the results to you. If a CI has been completed and failed and a +day or so has passed, it will be worth commenting in the issue to say you +cannot see what failed and to politely request in the PR that someone gives +you that information. + Ideally, the code change will pass ("be green") on all platform configurations supported by Node.js. This means that all tests pass and there are no linting errors. In reality, however, it is not uncommon for the CI infrastructure itself diff --git a/doc/contributing/releases.md b/doc/contributing/releases.md index 1f33d8f2771732..7f96cba0511c43 100644 --- a/doc/contributing/releases.md +++ b/doc/contributing/releases.md @@ -39,24 +39,47 @@ official release builds for Node.js, hosted on . ## Who can make a release? -Release authorization is given by the Node.js TSC. Once authorized, an -individual must have the following: +There is a distinction between individuals who can "prepare" a release by +adding the commits to the branches in GitHub and those who can "publish" the +releases to nodejs.org. + +Individuals who are Members of the +[backporters team](https://github.com/orgs/nodejs/teams/backporters) +(restricted link) can land things on the staging branches and prepare +releases including creating the proposal branches which will be discussed +later in this document. + +Authorization to publish releases is given by the Node.js TSC. This is +required to publish a release after it has been prepared. If you are working +on preparing a release for the first time you will be able to do most of the +steps as a member of the backporters team and have someone else who is +already onboarded promote the release on your behalf. Once authorized by the +TSC as a releaser, an individual will require the following to publish +releases themselves: ### 1. Jenkins release access -There are three relevant Jenkins jobs that should be used for a release flow: +There are four relevant Jenkins jobs that should be used for a release flow: **a.** **Test runs:** **[node-test-pull-request](https://ci.nodejs.org/job/node-test-pull-request/)** is used for a final full-test run to ensure that the current _HEAD_ is stable. -**b.** **Nightly builds:** (optional) +**b.** **CitGM:** +**[citgm-smoker](https://ci.nodejs.org/job/citgm-smoker/)** is used to run +the [CitGM](https://github.com/nodejs/citgm/) tool which tests a build of +Node.js against a defined set of community modules. This is used during a +release process to ensure that none of the commonly used modules which are +tested by CitGM show functional regressions with the new Node.js version +which could impact users. + +**c.** **Nightly builds:** (optional) **[iojs+release](https://ci-release.nodejs.org/job/iojs+release/)** can be used to create a nightly release for the current _HEAD_ if public test releases are required. Builds triggered with this job are published straight to and are available for public download. -**c.** **Release builds:** +**d.** **Release builds:** **[iojs+release](https://ci-release.nodejs.org/job/iojs+release/)** does all of the work to build all required release assets. Promotion of the release files is a manual step once they are ready (see below). @@ -66,8 +89,8 @@ this access to individuals authorized by the TSC. ### 2. \ access -The _dist_ user on nodejs.org controls the assets available in -. is an alias for +The _dist_ user on the `nodejs.org` host controls the assets available in +. is an alias for . The Jenkins release build workers upload their artifacts to the web server as @@ -90,6 +113,12 @@ responsible for that release. In order to be able to verify downloaded binaries, the public should be able to check that the `SHASUMS256.txt` file has been signed by someone who has been authorized to create a release. +If you do not currently have a key then you should create one with a +suitable strength with `gpg --full-generate-key`. The default options +(currently "ECC sign and encrypt" and "Curve 25519") are good choices and +consistent with the +[ssh key recommendations in the GOVERNANCE.md file](https://github.com/nodejs/Release/blob/main/GOVERNANCE.md#ssh-key-guidance). + The public keys should be fetchable from a known third-party keyserver. The OpenPGP keyserver at is recommended. Use the [submission](https://keys.openpgp.org/upload) form to submit @@ -108,11 +137,30 @@ gpg --keyserver hkps://keys.openpgp.org --recv-keys The key you use may be a child/subkey of an existing key. +If you wish to also upload your key to the commonly used Ubuntu keyservers +you can do so with + +```bash +gpg --keyserver keyserver.ubuntu.com --send-keys +``` + +and check it by switching the server name in the `--recv-keys` operation +list above to the Ubuntu keyserver. For more information take a look at the +[Ubuntu GPG howto wiki page](https://help.ubuntu.com/community/GnuPrivacyGuardHowto). + Additionally, full GPG key fingerprints for individuals authorized to release should be listed in the Node.js GitHub README.md file. -> It is recommended to sign all commits under the Node.js repository. -> Run: `git config commit.gpgsign true` inside the `node` folder. +> All commits to branches in `nodejs/node` other than `main` and `actions/*` MUST be signed +> otherwise pushing to those branches will be rejected by the GitHub rules +> enforced by the Node.js project. +> Run: `git config commit.gpgsign true` inside the `node` folder or use the +> `-S` flag on your git operations (The examples in this document will +> include `-S` expliticlty) + +While GitHub allows signing individual commits using an ssh key, +that is not covered here as this will not allow you to sign releases, so you +will need to set up a GPG signing key in GitHub. ## How to create a release @@ -123,7 +171,7 @@ Notes: * Version strings are listed below as _"vx.y.z"_ or _"x.y.z"_. Substitute for the release version. * Examples will use the fictional release version `1.2.3`. -* When preparing a security release, follow the security steps in the details +* When preparing a _security release_, follow the security steps in the details sections. ### 0. Pre-release steps @@ -136,13 +184,6 @@ and the release blog post is available on the project website. Build can be contacted best by opening up an issue on the [Build issue tracker][]. -When preparing a security release, contact Build at least two weekdays in -advance of the expected release. To ensure that the security patch(es) can be -properly tested, run a `node-test-pull-request` job against the `main` branch -of the `nodejs-private/node-private` repository a day or so before the -[CI lockdown procedure][] begins. This is to confirm that Jenkins can properly -access the private repository. - ### 1. Update the staging branch Checkout the staging branch locally. @@ -174,11 +215,11 @@ When landing the PR add the `Backport-PR-URL:` line to each commit. Close the backport PR with `Landed in ...`. Update the label on the original PR from `backport-requested-vN.x` to `backported-to-vN.x`. -You can add the `Backport-PR-URL` metadata by using `--backport` with -`git node land` +You can add the `Backport-PR-URL` metadata automatically when landing by +using `--backport` with `git node land`: ```bash -git node land --backport $PR-NUMBER +git node land -S --backport $PR-NUMBER ``` To determine the relevant commits, use @@ -189,16 +230,32 @@ metadata, as well as the GitHub labels such as `semver-minor` and omitted from a commit, the commit will show up because it's unsure if it's a duplicate or not. +A `branch-diff` run can use a lot of credits and users are +[limited by default to 5000 per hour](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2026-03-10). +It is not unusual for a run of branch-diff to +use around 1000 of these. For this reason it is recommended that when you +run branch-diff you redirect the output to a file and then process it. You +can check your current usage with `gh rate_limit` if you have the GitHub CLI +installed and configured, or using the curl command from +[this link](https://docs.github.com/en/rest/rate-limit/rate-limit?apiVersion=2026-03-10) +with authentication e.g. + +```bash +curl -H "Authorization: token $YOURGITHUBTOKEN" -X GET https://api.github.com/rate_limit +``` + For a list of commits that could be landed in a minor release on v1.x: ```bash N=1 sh -c 'branch-diff v$N.x-staging upstream/main --exclude-label=semver-major,dont-land-on-v$N.x,backport-requested-v$N.x,backport-blocked-v$N.x,backport-open-v$N.x,backported-to-v$N.x --filter-release --format=simple' ``` -If the target branch is an LTS line, you should also exclude the `baking-for-lts`: +If the target branch is an LTS line, you should also exclude the +`baking-for-lts` and use a Current version released at least two weeks before the expected release date. +In this example we use 25.5.0 as the base version to prepare a release for Node.js 24: ```bash -N=1 sh -c 'branch-diff v$N.x-staging upstream/main --exclude-label=semver-major,dont-land-on-v$N.x,backport-requested-v$N.x,backport-blocked-v$N.x,backport-open-v$N.x,backported-to-v$N.x,baking-for-lts --filter-release --format=simple' +N=24 sh -c 'branch-diff v$N.x-staging v26.5.0 --exclude-label=semver-major,dont-land-on-v$N.x,backport-requested-v$N.x,backport-blocked-v$N.x,backport-open-v$N.x,backported-to-v$N.x,baking-for-lts --filter-release --format=simple' ``` Previously released commits and version bumps do not need to be @@ -216,6 +273,11 @@ Carefully review the list of commits: When you are ready to cherry-pick commits, you can automate with the following command. +Since this is slightly different from the previous branch-diff output - it +contains only the commit SHAs and in revert order - you may wish to save +this before piping it directly to `git cherry-pick` in case it does not go +cleanly. + ```bash N=1 sh -c 'branch-diff v$N.x-staging upstream/main --exclude-label=semver-major,dont-land-on-v$N.x,backport-requested-v$N.x,backport-blocked-v$N.x,backport-open-v$N.x,backported-to-v$N.x --filter-release --format=sha --reverse' | xargs git cherry-pick -S ``` @@ -351,6 +413,8 @@ This will ensure they are included in the "Notable Changes" section of the CHANG ### 3. Update `src/node_version.h` +_(This step will be done automatically if you are using `create-release-proposal` or `git node release --prepare`)_ + Set the version for the proposed release using the following macros, which are already defined in `src/node_version.h`: @@ -369,6 +433,8 @@ be produced with a version string that does not have a trailing pre-release tag: ### 4. Update the changelog +_(This step will be done automatically if you are using `create-release-proposal` or `git node release --prepare`)_ + #### Step 1: Collect the formatted list of changes Collect a formatted list of commits since the last release. Use @@ -485,13 +551,15 @@ are formatted correctly. If this release includes new APIs then it is necessary to document that they were first added in this version. The relevant commits should already include `REPLACEME` tags (see [Writing Documentation](./api-documentation.md#writing-documentation)). -Check for these tags with ```bash grep REPLACEME doc/api/*.md ``` -and substitute this node version with +The above command will check for the presence of the tags and show you which +files need to be updated. You can then perform the replacements with one of +the following commands using either `sed` or `perl`. In these examples +`$VERSION` must be prefixed with a `v`: ```bash sed -i "s/REPLACEME/$VERSION/g" doc/api/*.md @@ -509,8 +577,6 @@ or perl -pi -e "s/REPLACEME/$VERSION/g" doc/api/*.md ``` -`$VERSION` should be prefixed with a `v`. - If this release includes any new deprecations it is necessary to ensure that those are assigned a proper static deprecation code. These are listed in the docs (see `doc/api/deprecations.md`) and in the source as `DEP00XX`. The code @@ -520,6 +586,8 @@ run. ### 5. Create release commit +_(This step will be done automatically if you are using `create-release-proposal` or `git node release --prepare`)_ + The `CHANGELOG.md`, `doc/changelogs/CHANGELOG_Vx.md`, `src/node_version.h`, and `REPLACEME` changes should be the final commit that will be tagged for the release. When committing these to git, use the following message format: @@ -561,6 +629,8 @@ Otherwise, you will leak the commits before the security release. ### 6. Propose release on GitHub +_(This step will be done automatically if you are using `create-release-proposal` or `git node release --prepare`)_ + Push the release branch to `nodejs/node`, not to your own fork. This allows release branches to more easily be passed between members of the release team if necessary. @@ -612,13 +682,18 @@ purpose. Run it once with the base `vx.x` branch as a reference and with the proposal branch to check if new regressions could be introduced in the ecosystem. -Use `ncu-ci` to compare `vx.x` run (10) and proposal branch (11) +Use `ncu-ci` with the two build numbers from the `citgm-smoker` job to +compare the base `vx.x` run (10) and the new proposal branch (11). ```bash npm i -g @node-core/utils ncu-ci citgm 10 11 ``` +A number of the modules tested by CitGM are not completely +reliable so differences shown by the comparison are not immediately cause +for concern. +
Security release @@ -744,7 +819,7 @@ Once you have produced builds that you're happy with you can either run `git node release --promote`: ```bash -git node release --promote https://github.com/nodejs/node/pull/XXXX -S +git node release -S --promote https://github.com/nodejs/node/pull/XXXX ``` to automate the remaining steps until step 16 or you can perform it manually @@ -760,11 +835,10 @@ fetch the proposal from using the `--fetch-from` flag. When promoting several releases, you can pass multiple URLs: ```bash -git node release --promote \ +git node release -S --promote \ --fetch-from git@github.com:nodejs-private/node-private.git \ https://github.com/nodejs-private/node-private/pull/XXXX \ - https://github.com/nodejs-private/node-private/pull/XXXX \ - -S + https://github.com/nodejs-private/node-private/pull/XXXX ```
@@ -1494,7 +1568,6 @@ Typical resolution: sign the release again. ``` [Build issue tracker]: https://github.com/nodejs/build/issues/new -[CI lockdown procedure]: https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#restricting-access-for-security-releases [Node.js Snap management repository]: https://github.com/nodejs/snap [Snap]: https://snapcraft.io/node [`create-release-post.yml`]: https://github.com/nodejs/nodejs.org/actions/workflows/create-release-post.yml diff --git a/doc/node.1 b/doc/node.1 index 03d55cc9d6b863..300f58ca4aa140 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -1069,9 +1069,6 @@ node --max-old-space-size-percentage=50 index.js node --max-old-space-size-percentage=75 index.js .Ed . -.It Fl -napi-modules -This option is a no-op. It is kept for compatibility. -. .It Fl -network-family-autoselection-attempt-timeout Sets the default value for the network family autoselection attempt timeout. For more information, see \fBnet.getDefaultAutoSelectFamilyAttemptTimeout()\fR. @@ -2039,8 +2036,6 @@ one is included in the list below. .It \fB--max-old-space-size-percentage\fR .It -\fB--napi-modules\fR -.It \fB--network-family-autoselection-attempt-timeout\fR .It \fB--no-addons\fR diff --git a/lib/ffi.js b/lib/ffi.js index 876e71334e3c2e..d91b6d4521e679 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -42,6 +42,7 @@ const { getUint64, getFloat32, getFloat64, + getCurrentEventLoop, exportBytes, getRawPointer, kFastArguments, @@ -320,6 +321,7 @@ module.exports = { getUint64, getFloat32, getFloat64, + getCurrentEventLoop, getRawPointer, setInt8, setUint8, diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index 7b9524ef855988..d9c123ee846b90 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -52,6 +52,8 @@ const { validateObject, } = require('internal/validators'); const { + kAutoEmptyTrailers, + kDisableAutoTrailers, kSocket, kRequest, kProxySocket, @@ -300,16 +302,23 @@ function onStreamCloseRequest() { req.emit('close'); } -function onStreamTimeout(kind) { - return function onStreamTimeout() { - const obj = this[kind]; - obj.emit('timeout'); - }; +// Shared between all Http2ServerRequest instances created without explicit +// options; the Readable constructor only reads from it. +const kDefaultRequestOptions = { autoDestroy: false }; + +function onStreamTimeoutRequest() { + this[kRequest].emit('timeout'); +} + +function onStreamTimeoutResponse() { + this[kResponse].emit('timeout'); } class Http2ServerRequest extends Readable { constructor(stream, headers, options, rawHeaders) { - super({ autoDestroy: false, ...options }); + super(options === undefined ? + kDefaultRequestOptions : + { autoDestroy: false, ...options }); this[kState] = { closed: false, didRead: false, @@ -332,7 +341,7 @@ class Http2ServerRequest extends Readable { stream.on('error', onStreamError); stream.on('aborted', onStreamAbortedRequest); stream.on('close', onStreamCloseRequest); - stream.on('timeout', onStreamTimeout(kRequest)); + stream.on('timeout', onStreamTimeoutRequest); this.on('pause', onRequestPause); this.on('resume', onRequestResume); } @@ -472,6 +481,8 @@ class Http2ServerResponse extends Stream { this[kState] = { closed: false, ending: false, + finishing: false, + hasTrailers: false, destroyed: false, headRequest: false, sendDate: true, @@ -488,7 +499,7 @@ class Http2ServerResponse extends Stream { stream.on('aborted', onStreamAbortedResponse); stream.on('close', onStreamCloseResponse); stream.on('wantTrailers', onStreamTrailersReady); - stream.on('timeout', onStreamTimeout(kResponse)); + stream.on('timeout', onStreamTimeoutResponse); } // User land modules such as finalhandler just check truthiness of this @@ -583,6 +594,15 @@ class Http2ServerResponse extends Stream { name = name.trim().toLowerCase(); assertValidHeader(name, value); this[kTrailers][name] = value; + const state = this[kState]; + if (!state.hasTrailers) { + state.hasTrailers = true; + // If the response headers were already flushed with auto-empty + // trailers enabled, tell the stream to hand the trailers back to JS. + const stream = this[kStream]; + if (stream.headersSent) + stream[kDisableAutoTrailers](); + } } addTrailers(headers) { @@ -838,6 +858,12 @@ class Http2ServerResponse extends Stream { return this; } + // If the headers have not been flushed yet, they will be flushed below + // as part of ending the response. In that case there is no further + // opportunity to add trailers, so the trailers round trip can be + // skipped entirely when none have been registered. + state.finishing = true; + if (chunk !== null && chunk !== undefined) this.write(chunk, encoding); @@ -895,9 +921,18 @@ class Http2ServerResponse extends Stream { const state = this[kState]; const headers = this[kHeaders]; headers[HTTP2_HEADER_STATUS] = state.statusCode; + // Only wait for trailers if some have been registered, or if the + // headers are flushed before the response is ended (in which case + // trailers may still be added during streaming). Trailers added after + // end() are dropped, matching HTTP/1 addTrailers() semantics. + const waitForTrailers = state.hasTrailers || !state.finishing; const options = { endStream: state.ending, - waitForTrailers: true, + waitForTrailers, + // When no trailers have been registered yet, let the native side + // finish the stream on its own if none show up by the time the last + // DATA frame is sent (see setTrailer()). + [kAutoEmptyTrailers]: waitForTrailers && !state.hasTrailers, sendDate: state.sendDate, }; this[kStream].respond(headers, options); diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 3361c955c2aad1..0691b4aabfc81b 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -150,6 +150,8 @@ const { getStreamState, isPayloadMeaningless, kAuthority, + kAutoEmptyTrailers, + kDisableAutoTrailers, kSensitiveHeaders, kStrictSingleValueFields, kSocket, @@ -334,6 +336,7 @@ const { STREAM_OPTION_EMPTY_PAYLOAD, STREAM_OPTION_GET_TRAILERS, + STREAM_OPTION_AUTO_EMPTY_TRAILERS, } = constants; const STREAM_FLAGS_PENDING = 0x0; @@ -559,29 +562,19 @@ function sessionListenerRemoved(name) { } // Also keep track of listeners for the Http2Stream instances, as some events -// are emitted on those objects. -function streamListenerAdded(name) { - const session = this[kSession]; - if (!session) return; - switch (name) { - case 'priority': - session[kNativeFields][kSessionPriorityListenerCount]++; - break; - case 'frameError': - session[kNativeFields][kSessionFrameErrorListenerCount]++; - break; - } -} - -function streamListenerRemoved(name) { - const session = this[kSession]; +// are emitted on those objects. Instead of subscribing to 'newListener' and +// 'removeListener' (which makes every listener add/remove on every stream +// emit an extra tracking event), Http2Stream overrides the EventEmitter +// methods and updates the counts directly. +function trackStreamListener(stream, name, delta) { + const session = stream[kSession]; if (!session) return; switch (name) { case 'priority': - session[kNativeFields][kSessionPriorityListenerCount]--; + session[kNativeFields][kSessionPriorityListenerCount] += delta; break; case 'frameError': - session[kNativeFields][kSessionFrameErrorListenerCount]--; + session[kNativeFields][kSessionFrameErrorListenerCount] += delta; break; } } @@ -595,6 +588,20 @@ function onPing(payload) { session.emit('ping', payload); } +function streamNaturalCloseSettled(stream) { + return (stream._readableState.endEmitted || + !!stream._readableState.errored) && + (stream._writableState.finished || + !!stream._writableState.errored); +} + +// Shared 'end'/'finish'/'error' listener for the natural-close path of +// onStreamClose(). Named and reused to avoid per-stream closures. +function maybeDestroyNaturalClose() { + if (!this.destroyed && streamNaturalCloseSettled(this)) + this.destroy(); +} + // Fired by C++ when nghttp2's on_stream_close fires. `peerReset` is // true when the peer sent a RST_STREAM frame - peer RST_STREAM(NO_ERROR) // is otherwise indistinguishable from a clean END_STREAM exchange at @@ -655,22 +662,14 @@ function onStreamClose(code, peerReset) { // errored readable won't fire 'end' - and on a Duplex a writable // error propagates to readable.errored, blocking 'end' too. Treat // either side's errored state as settled. - const readDone = () => stream._readableState.endEmitted || - !!stream._readableState.errored; - const writeDone = () => stream._writableState.finished || - !!stream._writableState.errored; - if (readDone() && writeDone()) { + if (streamNaturalCloseSettled(stream)) { stream.destroy(); return true; } - const maybeDestroy = () => { - if (!stream.destroyed && readDone() && writeDone()) - stream.destroy(); - }; - stream.once('end', maybeDestroy); - stream.once('finish', maybeDestroy); - stream.once('error', maybeDestroy); + stream.on('end', maybeDestroyNaturalClose); + stream.on('finish', maybeDestroyNaturalClose); + stream.on('error', maybeDestroyNaturalClose); if (stream[kSession][kType] === NGHTTP2_SESSION_SERVER && !stream[kState].didRead && stream.readableFlowing === null) { @@ -2015,12 +2014,14 @@ function streamOnPause() { this[kHandle].readStop(); } +function streamOnFinishMaybeDestroy() { + this[kMaybeDestroy](); +} + function afterShutdown(status) { const stream = this.handle[kOwner]; if (stream) { - stream.on('finish', () => { - stream[kMaybeDestroy](); - }); + stream.on('finish', streamOnFinishMaybeDestroy); } // Currently this status value is unused this.callback(); @@ -2045,6 +2046,47 @@ function shutdownWritable(callback) { return afterShutdown.call(req, 0); } +// Completes one of the two halves of a dispatched write (the write callback +// itself and the end-of-stream check); the stream machinery callback runs +// once both have finished. The state lives on stream[kState] because only a +// single write may be in flight at any given time. +function finishWrite(stream) { + const state = stream[kState]; + if (--state.writePending !== 0) + return; + const cb = state.writeCb; + state.writeCb = null; + const err = aggregateTwoErrors(state.endErr, state.writeErr); + state.writeErr = null; + state.endErr = null; + // writeGeneric does not destroy on error and + // we cannot enable autoDestroy, + // so make sure to destroy on error. + if (err) { + stream.destroy(err); + } + cb(err); +} + +// Runs on the tick after a write was dispatched: if the write turned out to +// be the last chunk of an ending writable, shut the writable side down right +// away so the final DATA frame can include the END_STREAM flag. +function endCheckNT(stream) { + const state = stream[kState]; + if (state.writeErr || + !stream._writableState.ending || + stream._writableState.buffered.length || + (state.flags & STREAM_FLAGS_HAS_TRAILERS)) { + finishWrite(stream); + return; + } + debugStreamObj(stream, 'shutting down writable on last write'); + shutdownWritable.call(stream, (err) => { + state.endErr = err; + finishWrite(stream); + }); +} + function finishSendTrailers(stream, headersList) { // The stream might be destroyed and in that case // there is nothing to do. @@ -2132,7 +2174,7 @@ function finishCloseStream(code) { // An Http2Stream is a Duplex stream that is backed by a // node::http2::Http2Stream handle implementing StreamBase. class Http2Stream extends Duplex { - constructor(session, options) { + constructor(session, options, hasHandle = false) { options.allowHalfOpen = true; options.decodeStrings = false; options.autoDestroy = false; @@ -2144,7 +2186,10 @@ class Http2Stream extends Duplex { // been assigned. this.cork(); this[kSession] = session; - session[kState].pendingStreams.add(this); + // Streams constructed with their native handle already available (e.g. + // server streams) are initialized immediately and never become pending. + if (!hasHandle) + session[kState].pendingStreams.add(this); // Allow our logic for determining whether any reads have happened to // work in all situations. This is similar to what we do in _http_incoming. @@ -2159,6 +2204,14 @@ class Http2Stream extends Duplex { writeQueueSize: 0, trailersReady: false, endAfterHeaders: false, + writeCb: null, + writeErr: null, + endErr: null, + writePending: 0, + endInProgress: false, + endCheckOwed: false, + shutdownWritableCalled: false, + fd: -1, }; // Fields used by the compat API to avoid megamorphisms. @@ -2166,9 +2219,53 @@ class Http2Stream extends Duplex { this[kProxySocket] = null; this.on('pause', streamOnPause); + } + + addListener(name, listener) { + const ret = super.addListener(name, listener); + if (name === 'priority' || name === 'frameError') + trackStreamListener(this, name, 1); + return ret; + } + + on(name, listener) { + const ret = super.on(name, listener); + if (name === 'priority' || name === 'frameError') + trackStreamListener(this, name, 1); + return ret; + } - this.on('newListener', streamListenerAdded); - this.on('removeListener', streamListenerRemoved); + prependListener(name, listener) { + const ret = super.prependListener(name, listener); + if (name === 'priority' || name === 'frameError') + trackStreamListener(this, name, 1); + return ret; + } + + removeListener(name, listener) { + if (name === 'priority' || name === 'frameError') { + const before = this.listenerCount(name); + const ret = super.removeListener(name, listener); + if (this.listenerCount(name) !== before) + trackStreamListener(this, name, -1); + return ret; + } + return super.removeListener(name, listener); + } + + removeAllListeners(name) { + let priority = 0; + let frameError = 0; + if (name === undefined || name === 'priority') + priority = this.listenerCount('priority'); + if (name === undefined || name === 'frameError') + frameError = this.listenerCount('frameError'); + const ret = super.removeAllListeners(name); + if (priority !== 0) + trackStreamListener(this, 'priority', -priority); + if (frameError !== 0) + trackStreamListener(this, 'frameError', -frameError); + return ret; } [kUpdateTimer]() { @@ -2346,45 +2443,40 @@ class Http2Stream extends Duplex { if (!this.headersSent) this[kProceed](); - let req; + // The stream machinery dispatches at most one _write()/_writev() at a + // time, so the coordination state between the write callback and the + // end-of-stream check below can live on the stream state instead of + // being captured by per-write closures. + const state = this[kState]; + state.writeCb = cb; + state.writeErr = null; + state.endErr = null; + + if (state.flags & STREAM_FLAGS_HAS_TRAILERS) { + // Trailers are pending, so the writable side cannot be shut down + // early anyway; there is no point in scheduling the end check. + state.writePending = 1; + } else if (state.endInProgress) { + // This write was dispatched from inside end(), which makes it the + // final chunk; end() runs the end-of-stream check synchronously once + // the stream machinery has settled, avoiding a nextTick per write. + state.writePending = 2; + state.endCheckOwed = true; + } else { + state.writePending = 2; + // Shutdown write stream right after last chunk is sent + // so final DATA frame can include END_STREAM flag + process.nextTick(endCheckNT, this); + } - let waitingForWriteCallback = true; - let waitingForEndCheck = true; - let writeCallbackErr; - let endCheckCallbackErr; - const done = () => { - if (waitingForEndCheck || waitingForWriteCallback) return; - const err = aggregateTwoErrors(endCheckCallbackErr, writeCallbackErr); - // writeGeneric does not destroy on error and - // we cannot enable autoDestroy, - // so make sure to destroy on error. - if (err) { - this.destroy(err); - } - cb(err); - }; + // This is invoked both as a method on the write req and as a plain + // call, so the stream has to be captured here. const writeCallback = (err) => { - waitingForWriteCallback = false; - writeCallbackErr = err; - done(); - }; - const endCheckCallback = (err) => { - waitingForEndCheck = false; - endCheckCallbackErr = err; - done(); + state.writeErr = err; + finishWrite(this); }; - // Shutdown write stream right after last chunk is sent - // so final DATA frame can include END_STREAM flag - process.nextTick(() => { - if (writeCallbackErr || - !this._writableState.ending || - this._writableState.buffered.length || - (this[kState].flags & STREAM_FLAGS_HAS_TRAILERS)) - return endCheckCallback(); - debugStreamObj(this, 'shutting down writable on last write'); - shutdownWritable.call(this, endCheckCallback); - }); + let req; if (writev) req = writevGeneric(this, data, writeCallback); else @@ -2410,6 +2502,25 @@ class Http2Stream extends Duplex { this[kWriteGeneric](true, data, '', cb); } + end(chunk, encoding, cb) { + const state = this[kState]; + // Any write dispatched while end() runs is the final chunk. Marking + // that lets [kWriteGeneric] hand its end-of-stream check back to us to + // run synchronously below (once the writable state has settled) + // instead of scheduling a nextTick for it. + state.endInProgress = true; + try { + super.end(chunk, encoding, cb); + } finally { + state.endInProgress = false; + if (state.endCheckOwed) { + state.endCheckOwed = false; + endCheckNT(this); + } + } + return this; + } + _final(cb) { if (this.pending) { this.once('ready', () => this._final(cb)); @@ -2439,6 +2550,17 @@ class Http2Stream extends Duplex { } } + // Called by the compat layer when trailers are registered after the + // response headers were already sent with auto-empty trailers enabled + // (see STREAM_OPTION_AUTO_EMPTY_TRAILERS); switches the stream back to + // JS-managed trailers. + [kDisableAutoTrailers]() { + const handle = this[kHandle]; + if (this.destroyed || handle === undefined) + return; + handle.disableAutoTrailers(); + } + sendTrailers(headers) { if (this.destroyed || this.closed) throw new ERR_HTTP2_INVALID_STREAM(); @@ -2561,10 +2683,16 @@ class Http2Stream extends Duplex { // gives the session the opportunity to clean itself up. The session // will destroy if it has been closed and there are no other open or // pending streams. Delay with setImmediate so we don't do it on the - // nghttp2 stack. - setImmediate(() => { - session[kMaybeDestroy](); - }); + // nghttp2 stack. When the session is not closed (or other streams are + // still around), [kMaybeDestroy] would be a no-op, so skip scheduling + // it altogether: session.close() runs its own check, and the native + // side notifies again through ongracefulclosecomplete once all pending + // data has been flushed. + if (session.closed && + sessionState.streams.size === 0 && + sessionState.pendingStreams.size === 0) { + setImmediate(sessionMaybeDestroy, session); + } if (err) { if (session[kType] === NGHTTP2_SESSION_CLIENT) { if (onClientStreamErrorChannel.hasSubscribers) { @@ -2615,6 +2743,8 @@ class Http2Stream extends Duplex { } } +Http2Stream.prototype.off = Http2Stream.prototype.removeListener; + // TODO(aduh95): remove this in future semver-major Http2Stream.prototype.priority = deprecate(function priority(options) { if (this.destroyed) @@ -2649,6 +2779,10 @@ function callStreamClose(stream) { stream.close(); } +function sessionMaybeDestroy(session) { + session[kMaybeDestroy](); +} + function prepareResponseHeaders(stream, headersParam, options) { let headers; let statusCode; @@ -2679,31 +2813,35 @@ function prepareResponseHeaders(stream, headersParam, options) { function prepareResponseHeadersObject(oldHeaders, options) { assertIsObject(oldHeaders, 'headers', ['Object', 'Array']); const headers = { __proto__: null }; + let statusCode; + let hasDate = false; if (oldHeaders !== null && oldHeaders !== undefined) { // This loop is here for performance reason. Do not change. + // The :status and date fields are picked up while copying so they do + // not have to be looked up again on the null-prototype copy. for (const key in oldHeaders) { if (ObjectHasOwn(oldHeaders, key)) { - headers[key] = oldHeaders[key]; + const value = oldHeaders[key]; + headers[key] = value; + if (key === HTTP2_HEADER_STATUS) + statusCode = value; + else if (key === HTTP2_HEADER_DATE) + hasDate = value != null; } } headers[kSensitiveHeaders] = oldHeaders[kSensitiveHeaders]; } - const statusCode = - headers[HTTP2_HEADER_STATUS] = - headers[HTTP2_HEADER_STATUS] | 0 || HTTP_STATUS_OK; + statusCode = headers[HTTP2_HEADER_STATUS] = statusCode | 0 || HTTP_STATUS_OK; - if (options.sendDate == null || options.sendDate) { - headers[HTTP2_HEADER_DATE] ??= utcDate(); + if (!hasDate && (options.sendDate == null || options.sendDate)) { + headers[HTTP2_HEADER_DATE] = utcDate(); } validatePreparedResponseHeaders(headers, statusCode); - return { - headers, - statusCode: headers[HTTP2_HEADER_STATUS], - }; + return { headers, statusCode }; } function prepareResponseHeadersArray(headers, options) { @@ -2961,12 +3099,14 @@ function afterOpen(session, options, headers, streamOptions, err, fd) { class ServerHttp2Stream extends Http2Stream { constructor(session, handle, id, options, headers) { - super(session, options); + super(session, options, true); handle.owner = this; this[kInit](id, handle); this[kProtocol] = headers[HTTP2_HEADER_SCHEME]; this[kAuthority] = getAuthority(headers); - this.once('finish', autoDrainReadable); + // 'finish' is only emitted once, so a regular listener is safe here and + // avoids allocating a once() wrapper for every stream. + this.on('finish', autoDrainReadable); } // True if the remote peer accepts push streams @@ -3086,20 +3226,27 @@ class ServerHttp2Stream extends Http2Stream { const state = this[kState]; assertIsObject(options, 'options'); - options = { ...options }; + // The options are only read, never mutated, so the user-provided object + // can be used directly instead of copying it. + options ??= kEmptyObject; debugStreamObj(this, 'initiating response'); this[kUpdateTimer](); - options.endStream = !!options.endStream; + const endStream = !!options.endStream; let streamOptions = 0; - if (options.endStream) + if (endStream) streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD; if (options.waitForTrailers) { streamOptions |= STREAM_OPTION_GET_TRAILERS; state.flags |= STREAM_FLAGS_HAS_TRAILERS; + // Internal mode used by the compat layer: if no trailers have been + // registered by the time the final DATA frame is sent, the native + // side finishes the stream without calling back into JS at all. + if (options[kAutoEmptyTrailers]) + streamOptions |= STREAM_OPTION_AUTO_EMPTY_TRAILERS; } const { @@ -3112,12 +3259,11 @@ class ServerHttp2Stream extends Http2Stream { // Close the writable side if the endStream option is set or status // is one of known codes with no payload, or it's a head request - if (!!options.endStream || + if (endStream || statusCode === HTTP_STATUS_NO_CONTENT || statusCode === HTTP_STATUS_RESET_CONTENT || statusCode === HTTP_STATUS_NOT_MODIFIED || this.headRequest === true) { - options.endStream = true; this.end(); } @@ -3307,7 +3453,7 @@ ServerHttp2Stream.prototype[kProceed] = ServerHttp2Stream.prototype.respond; class ClientHttp2Stream extends Http2Stream { constructor(session, handle, id, options) { - super(session, options); + super(session, options, id !== undefined); this[kState].flags |= STREAM_FLAGS_HEADERS_SENT; if (id !== undefined) this[kInit](id, handle); diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 25adc8f9697d82..159ba5bd531524 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -38,6 +38,8 @@ const { } = require('internal/errors'); const kAuthority = Symbol('authority'); +const kAutoEmptyTrailers = Symbol('autoEmptyTrailers'); +const kDisableAutoTrailers = Symbol('disableAutoTrailers'); const kSensitiveHeaders = Symbol('sensitiveHeaders'); const kStrictSingleValueFields = Symbol('strictSingleValueFields'); const kSocket = Symbol('socket'); @@ -770,14 +772,16 @@ function buildNgHeaderString(arrayOrMap, let pseudoHeaders = ''; let count = 0; - const singles = new SafeSet(); + let singles; const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray; - const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase()); + const neverIndex = sensitiveHeaders.length === 0 ? + emptyArray : sensitiveHeaders.map((v) => v.toLowerCase()); function processHeader(key, value) { key = key.toLowerCase(); + const isSingleValueField = kSingleValueFields.has(key); const isStrictSingleValueField = strictSingleValueFields && - kSingleValueFields.has(key); + isSingleValueField; let isArray = ArrayIsArray(value); if (isArray) { switch (value.length) { @@ -795,11 +799,15 @@ function buildNgHeaderString(arrayOrMap, value = String(value); } if (isStrictSingleValueField) { - if (singles.has(key)) + if (singles === undefined) { + singles = [key]; + } else if (singles.includes(key)) { throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key); - singles.add(key); + } else { + singles.push(key); + } } - const flags = neverIndex.includes(key) ? + const flags = neverIndex.length !== 0 && neverIndex.includes(key) ? kNeverIndexFlag : kNoHeaderFlags; if (key[0] === ':') { @@ -810,11 +818,15 @@ function buildNgHeaderString(arrayOrMap, count++; return; } - if (!checkIsHttpToken(key)) { - throw new ERR_INVALID_HTTP_TOKEN('Header name', key); - } - if (isIllegalConnectionSpecificHeader(key, value)) { - throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key); + // Well-known single-value fields are all valid HTTP tokens and none of + // them is a connection-specific header, so both checks can be skipped. + if (!isSingleValueField) { + if (!checkIsHttpToken(key)) { + throw new ERR_INVALID_HTTP_TOKEN('Header name', key); + } + if (isIllegalConnectionSpecificHeader(key, value)) { + throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key); + } } if (isArray) { for (let j = 0; j < value.length; ++j) { @@ -981,6 +993,8 @@ module.exports = { getStreamState, isPayloadMeaningless, kAuthority, + kAutoEmptyTrailers, + kDisableAutoTrailers, kSensitiveHeaders, kStrictSingleValueFields, kSocket, diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 99eeccc3c51d23..96362355c1deba 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2543,8 +2543,8 @@ class QuicStream { inner.ontrailers = undefined; inner.oninfo = undefined; inner.onwanttrailers = undefined; - inner.headers = undefined; - inner.pendingTrailers = undefined; + // Do not reset headers here, this is still important information + // the same applies for pendingTrailers this.#handle = undefined; if (inner.fileHandle !== undefined) { // Close the FileHandle that was used as a body source. The close @@ -3586,7 +3586,9 @@ class QuicSession { if (error) { // If the session is still waiting to be closed, and error - // is specified, reject the closed promise. + // is specified, reject the closed promise. Mark it handled first + // (as with opened) to silence errors if it's not actually awaited. + markPromiseAsHandled(inner.pendingClose.promise); inner.pendingClose.reject?.(error); } else { inner.pendingClose.resolve?.(); diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js index 7d89607fee0a3b..7a2d5719b9b97b 100644 --- a/lib/internal/streams/iter/consumers.js +++ b/lib/internal/streams/iter/consumers.js @@ -463,20 +463,11 @@ function merge(...args) { if (result.done) { activeCount--; } else { - ArrayPrototypePush(ready, result.value); - // Immediately request the next value from this source - // (at most one pending .next() per source) - PromisePrototypeThen( - iterator.next(), - (r) => onSettled(iterator, r), - (err) => { - ArrayPrototypePush(ready, { __proto__: null, error: err }); - if (waitResolve) { - waitResolve(); - waitResolve = null; - } - }, - ); + ArrayPrototypePush(ready, { + __proto__: null, + iterator, + value: result.value, + }); } if (waitResolve) { waitResolve(); @@ -513,7 +504,18 @@ function merge(...args) { if (item?.error) { throw item.error; } - yield item; + yield item.value; + PromisePrototypeThen( + item.iterator.next(), + (r) => onSettled(item.iterator, r), + (err) => { + ArrayPrototypePush(ready, { __proto__: null, error: err }); + if (waitResolve) { + waitResolve(); + waitResolve = null; + } + }, + ); } // If sources are still active, wait for the next settlement diff --git a/lib/internal/streams/iter/from.js b/lib/internal/streams/iter/from.js index 1efe83e9a04162..3277760f5f0a9d 100644 --- a/lib/internal/streams/iter/from.js +++ b/lib/internal/streams/iter/from.js @@ -484,15 +484,20 @@ function fromSync(input) { return fromSync(input[toStreamable]()); } - // Reject explicit async inputs - if (isAsyncIterable(input)) { + const isIterable = isSyncIterable(input); + + // Reject explicit async-only inputs + if (!isIterable && isAsyncIterable(input)) { throw new ERR_INVALID_ARG_TYPE( 'input', 'a synchronous input (not AsyncIterable)', input, ); } - if (typeof input === 'object' && input !== null && typeof input.then === 'function') { + if (!isIterable && + typeof input === 'object' && + input !== null && + typeof input.then === 'function') { throw new ERR_INVALID_ARG_TYPE( 'input', 'a synchronous input (not Promise)', @@ -501,7 +506,7 @@ function fromSync(input) { } // Must be a SyncStreamable - if (!isSyncIterable(input)) { + if (!isIterable) { throw new ERR_INVALID_ARG_TYPE( 'input', ['string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable', 'toStreamable'], diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index b8c07443e93e2a..b565d1d86b9892 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -679,15 +679,17 @@ Readable.prototype.read = function(n) { // If we're doing read(0) to trigger a readable event, but we // already have a bunch of data in the buffer, then just trigger - // the 'readable' event and move on. + // the 'readable' event and move on. `state.length` cannot change + // within this block, so it is loaded once instead of three times. + const stateLength = state.length; if (n === 0 && (state[kState] & kNeedReadable) !== 0 && ((state.highWaterMark !== 0 ? - state.length >= state.highWaterMark : - state.length > 0) || + stateLength >= state.highWaterMark : + stateLength > 0) || (state[kState] & kEnded) !== 0)) { debug('read: emitReadable'); - if (state.length === 0 && (state[kState] & kEnded) !== 0) + if (stateLength === 0 && (state[kState] & kEnded) !== 0) endReadable(this); else emitReadable(this); @@ -1635,8 +1637,14 @@ Readable._fromList = fromList; // This function is designed to be inlinable, so please take care when making // changes to the function body. function fromList(n, state) { + // `state.length` cannot change while this function runs (only the + // caller updates it, after this returns) and the chunk lengths feeding + // the copy loops cannot change across the copy calls, so every + // repeated property load below is hoisted into a local. + const stateLength = state.length; + // nothing buffered. - if (state.length === 0) + if (stateLength === 0) return null; let idx = state.bufferIndex; @@ -1648,7 +1656,7 @@ function fromList(n, state) { if ((state[kState] & kObjectMode) !== 0) { ret = buf[idx]; buf[idx++] = null; - } else if (!n || n >= state.length) { + } else if (!n || n >= stateLength) { // Read it all, truncate the list. if ((state[kState] & kDecoder) !== 0) { ret = ''; @@ -1662,61 +1670,68 @@ function fromList(n, state) { ret = buf[idx]; buf[idx++] = null; } else { - ret = Buffer.allocUnsafe(state.length); + ret = Buffer.allocUnsafe(stateLength); let i = 0; while (idx < len) { - TypedArrayPrototypeSet(ret, buf[idx], i); - i += buf[idx].length; + const data = buf[idx]; + TypedArrayPrototypeSet(ret, data, i); + i += data.length; buf[idx++] = null; } } - } else if (n < buf[idx].length) { - // `slice` is the same for buffers and strings. - ret = buf[idx].slice(0, n); - buf[idx] = buf[idx].slice(n); - } else if (n === buf[idx].length) { - // First chunk is a perfect match. - ret = buf[idx]; - buf[idx++] = null; - } else if ((state[kState] & kDecoder) !== 0) { - ret = ''; - while (idx < len) { - const str = buf[idx]; - if (n > str.length) { - ret += str; - n -= str.length; - buf[idx++] = null; - } else { - if (n === str.length) { + } else { + const first = buf[idx]; + const firstLength = first.length; + if (n < firstLength) { + // `slice` is the same for buffers and strings. + ret = first.slice(0, n); + buf[idx] = first.slice(n); + } else if (n === firstLength) { + // First chunk is a perfect match. + ret = first; + buf[idx++] = null; + } else if ((state[kState] & kDecoder) !== 0) { + ret = ''; + while (idx < len) { + const str = buf[idx]; + const strLength = str.length; + if (n > strLength) { ret += str; + n -= strLength; buf[idx++] = null; } else { - ret += str.slice(0, n); - buf[idx] = str.slice(n); + if (n === strLength) { + ret += str; + buf[idx++] = null; + } else { + ret += str.slice(0, n); + buf[idx] = str.slice(n); + } + break; } - break; } - } - } else { - ret = Buffer.allocUnsafe(n); - - const retLen = n; - while (idx < len) { - const data = buf[idx]; - if (n > data.length) { - TypedArrayPrototypeSet(ret, data, retLen - n); - n -= data.length; - buf[idx++] = null; - } else { - if (n === data.length) { + } else { + ret = Buffer.allocUnsafe(n); + + const retLen = n; + while (idx < len) { + const data = buf[idx]; + const dataLength = data.length; + if (n > dataLength) { TypedArrayPrototypeSet(ret, data, retLen - n); + n -= dataLength; buf[idx++] = null; } else { - TypedArrayPrototypeSet(ret, new FastBuffer(data.buffer, data.byteOffset, n), retLen - n); - buf[idx] = new FastBuffer(data.buffer, data.byteOffset + n, data.length - n); + if (n === dataLength) { + TypedArrayPrototypeSet(ret, data, retLen - n); + buf[idx++] = null; + } else { + TypedArrayPrototypeSet(ret, new FastBuffer(data.buffer, data.byteOffset, n), retLen - n); + buf[idx] = new FastBuffer(data.buffer, data.byteOffset + n, dataLength - n); + } + break; } - break; } } } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index a4441ea31a9dc9..7f50cc8521bcda 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -295,6 +295,10 @@ class FileTest extends Test { ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex)); } #handleReportItem(item) { + // The name is empty when a single child process runs all test files. + if (this.name !== '') { + item.data.entryFile = this.loc.file; + } const isTopLevel = item.data.nesting === 0; if (isTopLevel) { if (item.type === 'test:plan' && this.#skipReporting()) { diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index e0a84f30ec8380..c5a2c437d9b78b 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -113,9 +113,11 @@ const { extractSizeAlgorithm, getNonWritablePropertyDescriptor, isBrandCheck, + kEmptyQueue, kState, kType, lazyTransfer, + materializeQueue, nonOpCancel, nonOpPull, nonOpStart, @@ -501,6 +503,12 @@ class ReadableStream { current: undefined, }; let started = false; + // A single reusable read request: at most one read is ever in flight + // (next() chains through state.current), and the request is consumed + // before the next read starts, so only its promise record changes + // per read. + // eslint-disable-next-line no-use-before-define + const readRequest = new ReadableStreamAsyncIteratorReadRequest(reader, state, undefined); // The nextSteps function is not an async function in order // to make it more efficient. Because nextSteps explicitly @@ -519,8 +527,8 @@ class ReadableStream { } const promise = PromiseWithResolvers(); - // eslint-disable-next-line no-use-before-define - readableStreamDefaultReaderRead(reader, new ReadableStreamAsyncIteratorReadRequest(reader, state, promise)); + readRequest.promise = promise; + readableStreamDefaultReaderRead(reader, readRequest); return promise.promise; } @@ -574,28 +582,38 @@ class ReadableStream { } // No read is in flight. Mirror the buffered fast path of // ReadableStreamDefaultReader.read(): when data is already queued - // in a default controller, resolve immediately without allocating - // a read request. The result settles synchronously, so leaving + // in the controller, resolve immediately without allocating a + // read request. The result settles synchronously, so leaving // state.current undefined matches the state the slow path reaches // once its read request callbacks have settled. const stream = reader[kState].stream; - if (!state.done && stream !== undefined) { + if (!state.done && stream !== undefined && + stream[kState].state === 'readable') { const controller = stream[kState].controller; - if (stream[kState].state === 'readable' && - isReadableStreamDefaultController(controller) && - controller[kState].queue.length > 0) { - stream[kState].disturbed = true; - const chunk = dequeueValue(controller); - - if (controller[kState].closeRequested && - !controller[kState].queue.length) { - readableStreamDefaultControllerClearAlgorithms(controller); - readableStreamClose(stream); - } else { - readableStreamDefaultControllerCallPullIfNeeded(controller); + if (isReadableStreamDefaultController(controller)) { + if (controller[kState].queue.length > 0) { + stream[kState].disturbed = true; + const chunk = dequeueValue(controller); + + if (controller[kState].closeRequested && + !controller[kState].queue.length) { + readableStreamDefaultControllerClearAlgorithms(controller); + readableStreamClose(stream); + } else { + readableStreamDefaultControllerCallPullIfNeeded(controller); + } + + return PromiseResolve({ done: false, value: chunk }); } + } else if (controller[kState].queueTotalSize > 0) { + // Byte controller with buffered data: same shape as above via + // the queue-filled arm of the byte controller's pull steps. + stream[kState].disturbed = true; + return PromiseResolve({ + done: false, - return PromiseResolve({ done: false, value: chunk }); + value: readableByteStreamControllerDequeueChunk(controller), + }); } } state.current = nextSteps(); @@ -918,24 +936,36 @@ class ReadableStreamDefaultReader { const stream = this[kState].stream; const controller = stream[kState].controller; - // Fast path: if data is already buffered in a default controller, + // Fast path: if data is already buffered in the controller's queue, // return a resolved promise immediately without creating a read request. // This is spec-compliant because read() returns a Promise, and // Promise.resolve() callbacks still run in the microtask queue. - if (stream[kState].state === 'readable' && - isReadableStreamDefaultController(controller) && - controller[kState].queue.length > 0) { - stream[kState].disturbed = true; - const chunk = dequeueValue(controller); + if (stream[kState].state === 'readable') { + if (isReadableStreamDefaultController(controller)) { + if (controller[kState].queue.length > 0) { + stream[kState].disturbed = true; + const chunk = dequeueValue(controller); + + if (controller[kState].closeRequested && !controller[kState].queue.length) { + readableStreamDefaultControllerClearAlgorithms(controller); + readableStreamClose(stream); + } else { + readableStreamDefaultControllerCallPullIfNeeded(controller); + } - if (controller[kState].closeRequested && !controller[kState].queue.length) { - readableStreamDefaultControllerClearAlgorithms(controller); - readableStreamClose(stream); - } else { - readableStreamDefaultControllerCallPullIfNeeded(controller); + return PromiseResolve({ done: false, value: chunk }); + } + } else if (controller[kState].queueTotalSize > 0) { + // Byte controller with buffered data: mirror the queue-filled arm + // of its pull steps (which never consults pendingPullIntos) minus + // the read request. + stream[kState].disturbed = true; + return PromiseResolve({ + done: false, + + value: readableByteStreamControllerDequeueChunk(controller), + }); } - - return PromiseResolve({ done: false, value: chunk }); } // Slow path: create request and go through normal flow @@ -2495,6 +2525,11 @@ function readableStreamDefaultControllerEnqueue(controller, chunk) { reader[kType] === 'ReadableStreamDefaultReader' && reader[kState].readRequests.length) { readableStreamFulfillReadRequest(stream, chunk, false); + } else if (controllerState.sizeAlgorithm === defaultSizeAlgorithm) { + // The internal default size algorithm is never observable by user + // code, always returns 1, and cannot throw: enqueue with the + // constant instead of calling it. + enqueueValueWithSize(controller, chunk, 1); } else { try { const chunkSize = @@ -2652,7 +2687,7 @@ function setupReadableStreamDefaultController( pulling: false, pullFulfilled: undefined, pullRejected: undefined, - queue: [], + queue: kEmptyQueue, queueTotalSize: 0, started: false, sizeAlgorithm, @@ -3044,9 +3079,23 @@ function readableByteStreamControllerEnqueue(controller, chunk) { } } - if (readableStreamHasDefaultReader(stream)) { - readableByteStreamControllerProcessReadRequestsUsingQueue(controller); - if (!readableStreamGetNumReadRequests(stream)) { + // Single consolidated pass over the reader state. The spec routes this + // through HasDefaultReader / ProcessReadRequestsUsingQueue / + // GetNumReadRequests / FulfillReadRequest, which would re-run the same + // reader brand check and re-load the read request list four times on + // this per-chunk path. + const { reader } = stream[kState]; + if (reader !== undefined && + reader[kState] !== undefined && + reader[kType] === 'ReadableStreamDefaultReader') { + const { readRequests } = reader[kState]; + if (readRequests.length && controller[kState].queueTotalSize > 0) { + // Only possible when data was enqueued while the stream was not + // being read; read requests otherwise never coexist with a + // non-empty queue. + readableByteStreamControllerProcessReadRequestsUsingQueue(controller); + } + if (!readRequests.length) { readableByteStreamControllerEnqueueChunkToQueue( controller, transferredBuffer, @@ -3060,7 +3109,8 @@ function readableByteStreamControllerEnqueue(controller, chunk) { } const transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength); - readableStreamFulfillReadRequest(stream, transferredView, false); + const readRequest = ArrayPrototypeShift(readRequests); + readRequest[kChunk](transferredView); } } else if (readableStreamHasBYOBReader(stream)) { readableByteStreamControllerEnqueueChunkToQueue( @@ -3112,14 +3162,13 @@ function readableByteStreamControllerEnqueueChunkToQueue( buffer, byteOffset, byteLength) { - ArrayPrototypePush( - controller[kState].queue, - { - buffer, - byteOffset, - byteLength, - }); - controller[kState].queueTotalSize += byteLength; + const state = controller[kState]; + materializeQueue(state).push({ + buffer, + byteOffset, + byteLength, + }); + state.queueTotalSize += byteLength; } function readableByteStreamControllerEnqueueDetachedPullIntoToQueue( @@ -3174,7 +3223,7 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue( } = controller[kState]; while (totalBytesToCopyRemaining) { - const headOfQueue = queue[0]; + const headOfQueue = queue.peek(); const bytesToCopy = MathMin( totalBytesToCopyRemaining, headOfQueue.byteLength); @@ -3192,7 +3241,7 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue( headOfQueue.byteOffset, bytesToCopy); if (headOfQueue.byteLength === bytesToCopy) { - ArrayPrototypeShift(queue); + queue.shift(); } else { headOfQueue.byteOffset += bytesToCopy; headOfQueue.byteLength -= bytesToCopy; @@ -3395,22 +3444,28 @@ function readableByteStreamControllerCancelSteps(controller, reason) { return result; } -function readableByteStreamControllerFillReadRequestFromQueue(controller, readRequest) { - const { - queue, - queueTotalSize, - } = controller[kState]; - assert(queueTotalSize > 0); +// Dequeues the first chunk of the byte queue as a Uint8Array view, +// handling queue drain (close-on-empty or pull) before the view is +// created. This is the [[queueTotalSize]] > 0 arm of the byte +// controller's pull steps; it is also called directly from the +// buffered fast paths in ReadableStreamDefaultReader.read() and the +// async iterator, which resolve with the view without allocating a +// read request. +function readableByteStreamControllerDequeueChunk(controller) { + assert(controller[kState].queueTotalSize > 0); const { buffer, byteOffset, byteLength, - } = ArrayPrototypeShift(queue); + } = controller[kState].queue.shift(); controller[kState].queueTotalSize -= byteLength; readableByteStreamControllerHandleQueueDrain(controller); - const view = new Uint8Array(buffer, byteOffset, byteLength); - readRequest[kChunk](view); + return new Uint8Array(buffer, byteOffset, byteLength); +} + +function readableByteStreamControllerFillReadRequestFromQueue(controller, readRequest) { + readRequest[kChunk](readableByteStreamControllerDequeueChunk(controller)); } function readableByteStreamControllerProcessReadRequestsUsingQueue(controller) { @@ -3498,7 +3553,7 @@ function setupReadableByteStreamController( pullRejected: undefined, started: false, stream, - queue: [], + queue: kEmptyQueue, queueTotalSize: 0, highWaterMark, pullAlgorithm, diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js index cd203b77c2d22a..a27c61ed5e5db7 100644 --- a/lib/internal/webstreams/util.js +++ b/lib/internal/webstreams/util.js @@ -1,20 +1,25 @@ 'use strict'; const { + Array, ArrayBufferPrototypeGetByteLength, ArrayBufferPrototypeGetDetached, ArrayBufferPrototypeSlice, - ArrayPrototypePush, - ArrayPrototypeShift, AsyncIteratorPrototype, + DataViewPrototypeGetBuffer, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, FunctionPrototypeCall, MathMax, NumberIsNaN, + ObjectFreeze, PromisePrototypeThen, PromiseReject, PromiseResolve, - ReflectGet, Symbol, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, Uint8Array, } = primordials; @@ -41,6 +46,10 @@ const { const assert = require('internal/assert'); +const { + isDataView, +} = require('internal/util/types'); + const { validateFunction, } = require('internal/validators'); @@ -93,20 +102,29 @@ function customInspect(depth, options, name, data) { return `${name} ${inspect(data, opts)}`; } -// These are defensive to work around the possibility that -// the buffer, byteLength, and byteOffset properties on -// ArrayBuffer and ArrayBufferView's may have been tampered with. +// These use the original prototype getters so that user tampering with +// the buffer, byteLength, and byteOffset properties on ArrayBuffer and +// ArrayBufferView's is not observed. They run once or more per chunk on +// every byte-stream path, so they must not go through a reflective get +// (the previous view.constructor.prototype lookup was both slower and +// spoofable via a user-defined .constructor). function ArrayBufferViewGetBuffer(view) { - return ReflectGet(view.constructor.prototype, 'buffer', view); + return isDataView(view) ? + DataViewPrototypeGetBuffer(view) : + TypedArrayPrototypeGetBuffer(view); } function ArrayBufferViewGetByteLength(view) { - return ReflectGet(view.constructor.prototype, 'byteLength', view); + return isDataView(view) ? + DataViewPrototypeGetByteLength(view) : + TypedArrayPrototypeGetByteLength(view); } function ArrayBufferViewGetByteOffset(view) { - return ReflectGet(view.constructor.prototype, 'byteOffset', view); + return isDataView(view) ? + DataViewPrototypeGetByteOffset(view) : + TypedArrayPrototypeGetByteOffset(view); } function cloneAsUint8Array(view) { @@ -134,6 +152,143 @@ function isBrandCheck(brand) { }; } +// Backing store for the spec's [[queue]]: a power-of-two ring buffer +// instead of a plain array. Entries are pushed at the tail and consumed +// at the head, so a plain array either moves every element or forces the +// engine to re-linearize on each shift, and the default controllers would +// additionally have to allocate a { value, size } wrapper object per +// chunk just to keep the pair together. Default readable/writable +// controller queues store each entry as (value, size) in two consecutive +// slots via the *Pair methods; the readable byte controller queue stores +// its chunk descriptor records in single slots via push/shift/peek. A +// given instance only ever uses one of the two access patterns, so +// head/tail stay aligned to the entry stride. +class Queue { + constructor(listLength = 8) { + this.head = 0; + this.tail = 0; + // Number of logical entries currently in the queue: (value, size) + // pairs for default controller queues, descriptor records for byte + // controller queues. + this.length = 0; + this.capacityMask = listLength - 1; + this.list = new Array(listLength); + this.dequeuedSize = 0; + } + + // Single-slot entries (readable byte controller chunk records). + + push(entry) { + const tail = this.tail; + this.list[tail] = entry; + this.tail = (tail + 1) & this.capacityMask; + this.length++; + if (this.tail === this.head) + this.grow(); + } + + shift() { + const head = this.head; + const list = this.list; + const entry = list[head]; + list[head] = undefined; + this.head = (head + 1) & this.capacityMask; + if (--this.length === 0) + this.rewind(); + return entry; + } + + peek() { + return this.list[this.head]; + } + + // Two-slot (value, size) entries (default controller queues). The + // stride is always 2 and capacities are even, so `tail + 1`/`head + 1` + // never need to wrap. + + pushPair(value, size) { + const tail = this.tail; + const list = this.list; + list[tail] = value; + list[tail + 1] = size; + this.tail = (tail + 2) & this.capacityMask; + this.length++; + if (this.tail === this.head) + this.grow(); + } + + // Returns the dequeued value; the size of the same entry is left in + // `this.dequeuedSize` so that callers can update [[queueTotalSize]] + // without a per-entry wrapper object having to exist. + shiftPair() { + const head = this.head; + const list = this.list; + const value = list[head]; + this.dequeuedSize = list[head + 1]; + list[head] = undefined; + list[head + 1] = undefined; + this.head = (head + 2) & this.capacityMask; + if (--this.length === 0) + this.rewind(); + return value; + } + + peekPairValue() { + return this.list[this.head]; + } + + // The ring is completely full (the post-push tail caught up with the + // head): double the capacity, re-linearizing from the head so index + // arithmetic stays trivial. + grow() { + const list = this.list; + const capacity = list.length; + const head = this.head; + if (head !== 0) { + const relinearized = new Array(capacity * 2); + let n = 0; + for (let i = head; i < capacity; i++) + relinearized[n++] = list[i]; + for (let i = 0; i < head; i++) + relinearized[n++] = list[i]; + this.list = relinearized; + this.head = 0; + } else { + list.length = capacity * 2; + } + this.tail = capacity; + this.capacityMask = (capacity * 2) - 1; + } + + // The queue just became empty: restart at slot 0 so shallow queues net + // sequential slot access, and drop the enlarged backing store after a + // large burst has fully drained. + rewind() { + this.head = 0; + this.tail = 0; + if (this.list.length > 1024) { + this.list.length = 8; + this.capacityMask = 0b111; + } + } +} + +// Controllers start out with (and are reset to) this shared immutable +// empty queue, so constructing a stream never allocates queue storage; +// a real Queue is materialized by the enqueue paths on first use. All +// dequeue/peek paths are guarded by `.length` (or the equivalent +// [[queueTotalSize]]) checks, so they can never observe the sentinel in +// a mutating way; it never stores entries, so it gets a zero-length +// backing list. +const kEmptyQueue = ObjectFreeze(new Queue(0)); + +function materializeQueue(state) { + const queue = state.queue; + if (queue === kEmptyQueue) + return state.queue = new Queue(); + return queue; +} + // The queue helpers below run once per chunk on the hot paths of every // default readable/writable stream, so they load the controller state a // single time and don't assert the existence of the queue fields (both @@ -141,25 +296,23 @@ function isBrandCheck(brand) { // replaced wholesale). function dequeueValue(controller) { const state = controller[kState]; - assert(state.queue.length); - const { - value, - size, - } = ArrayPrototypeShift(state.queue); - state.queueTotalSize = MathMax(0, state.queueTotalSize - size); + const queue = state.queue; + assert(queue.length); + const value = queue.shiftPair(); + state.queueTotalSize = MathMax(0, state.queueTotalSize - queue.dequeuedSize); return value; } function resetQueue(controller) { const state = controller[kState]; - state.queue = []; + state.queue = kEmptyQueue; state.queueTotalSize = 0; } function peekQueueValue(controller) { const state = controller[kState]; assert(state.queue.length); - return state.queue[0].value; + return state.queue.peekPairValue(); } function enqueueValueWithSize(controller, value, size) { @@ -170,7 +323,7 @@ function enqueueValueWithSize(controller, value, size) { coercedSize === Infinity) { throw new ERR_INVALID_ARG_VALUE.RangeError('size', size); } - ArrayPrototypePush(state.queue, { value, size: coercedSize }); + materializeQueue(state).pushPair(value, coercedSize); state.queueTotalSize += coercedSize; } @@ -266,9 +419,11 @@ module.exports = { getNonWritablePropertyDescriptor, isBrandCheck, isPromisePending, + kEmptyQueue, kState, kType, lazyTransfer, + materializeQueue, nonOpCancel, nonOpFlush, nonOpPull, diff --git a/lib/internal/webstreams/writablestream.js b/lib/internal/webstreams/writablestream.js index 9a6af1aa4a1061..53b339e47fa798 100644 --- a/lib/internal/webstreams/writablestream.js +++ b/lib/internal/webstreams/writablestream.js @@ -67,6 +67,7 @@ const { getNonWritablePropertyDescriptor, isBrandCheck, isPromisePending, + kEmptyQueue, kState, kType, lazyTransfer, @@ -1177,6 +1178,11 @@ function writableStreamDefaultControllerGetChunkSize(controller, chunk) { return 1; } + // The internal default size algorithm is never observable by user + // code, always returns 1, and cannot throw: skip the call. + if (sizeAlgorithm === defaultSizeAlgorithm) + return 1; + try { return FunctionPrototypeCall( sizeAlgorithm, @@ -1293,7 +1299,7 @@ function setupWritableStreamDefaultController( abortAlgorithm, closeAlgorithm, highWaterMark, - queue: [], + queue: kEmptyQueue, queueTotalSize: 0, abortController: new AbortController(), sizeAlgorithm, diff --git a/lib/internal/worker/io.js b/lib/internal/worker/io.js index 786ee36c1927fa..4182538561fb9e 100644 --- a/lib/internal/worker/io.js +++ b/lib/internal/worker/io.js @@ -48,6 +48,7 @@ const { } = internalBinding('messaging'); const { getEnvMessagePort, + threadId, } = internalBinding('worker'); const { Readable, Writable } = require('stream'); @@ -346,7 +347,7 @@ function receiveMessageOnPort(port) { } function onMessageEvent(type, data) { - this.dispatchEvent(lazyMessageEvent(type, { data })); + this.dispatchEvent(lazyMessageEvent(type, { data: data.value, source: data.source })); } function isBroadcastChannel(value) { @@ -419,13 +420,17 @@ class BroadcastChannel extends EventTarget { * @returns {void} */ postMessage(message) { + const broadcastMessage = { + source: threadId, + value: message, + }; if (!isBroadcastChannel(this)) throw new ERR_INVALID_THIS('BroadcastChannel'); if (arguments.length === 0) throw new ERR_MISSING_ARGS('message'); if (this[kHandle] === undefined) throw new DOMException('BroadcastChannel is closed.', 'InvalidStateError'); - if (this[kHandle].postMessage(message) === undefined) + if (this[kHandle].postMessage(broadcastMessage) === undefined) throw new DOMException('Message could not be posted.'); } diff --git a/lib/tls.js b/lib/tls.js index 283e5f5bc7870a..296f6189da1728 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -431,8 +431,14 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) { let valid = false; let reason = 'Unknown reason'; - if (net.isIP(hostnameASCIIWithoutFQDN)) { - valid = ips.includes(canonicalizeIP(hostnameASCIIWithoutFQDN)); + // An IP literal must not be IDNA-normalized: domainToASCII() returns '' for + // an IPv6 literal (it is not a domain), so matching against the normalized + // host would skip IP-SAN matching for IPv6 entirely. Match IP hosts against + // the original hostname (net.isIP() rejects non-ASCII, so there is no IDNA + // confusion to guard against here); the normalized form is kept for the + // DNS-name path below. + if (net.isIP(hostname)) { + valid = ips.includes(canonicalizeIP(hostname)); if (!valid) { reason = `IP: ${hostname} is not in the cert's list: ` + ips.join(', '); diff --git a/src/crypto/crypto_common.cc b/src/crypto/crypto_common.cc index 0db5d0eaac8fc3..fde12953860cd0 100644 --- a/src/crypto/crypto_common.cc +++ b/src/crypto/crypto_common.cc @@ -231,11 +231,15 @@ MaybeLocal GetEphemeralKey(Environment* env, const SSLPointer& ssl) { case EVP_PKEY_X448: { const char* curve_name; if (kid == EVP_PKEY_EC) { - int nid = ECKeyPointer::GetGroupName(key); + ECKeyPointer ec(key); + if (!ec) break; + int nid = EC_GROUP_get_curve_name(ec.getGroup()); + if (nid == NID_undef) break; curve_name = OBJ_nid2sn(nid); } else { curve_name = OBJ_nid2sn(kid); } + if (curve_name == nullptr) break; values[0] = env->ecdh_string(); values[1] = OneByteString(env->isolate(), curve_name); values[2] = Integer::New(env->isolate(), key.bits()); @@ -291,7 +295,7 @@ MaybeLocal GetPeerCert( // NOTE: This is because of the odd OpenSSL behavior. On client `cert_chain` // contains the `peer_certificate`, but on server it doesn't. - X509Pointer cert(is_server ? SSL_get_peer_certificate(ssl.get()) : nullptr); + X509Pointer cert(is_server ? X509Pointer::PeerFrom(ssl) : X509Pointer()); STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(ssl.get()); if (!cert && (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0)) return Undefined(env->isolate()); diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 01d8a17d8e2f53..5447de596f98ec 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -18,9 +18,6 @@ #include #include #include -#ifndef OPENSSL_NO_ENGINE -#include -#endif // !OPENSSL_NO_ENGINE #ifdef __APPLE__ #include #endif @@ -34,7 +31,6 @@ namespace node { -using ncrypto::BignumPointer; using ncrypto::BIOPointer; using ncrypto::Cipher; using ncrypto::ClearErrorOnReturn; @@ -549,7 +545,7 @@ static bool IsCertificateExpired(X509* cert) { // -1 if the time is in the past (expired) // 0 if there was an error // 1 if the time is in the future (not yet expired) - ASN1_TIME* not_after = X509_get_notAfter(cert); + const ASN1_TIME* not_after = X509_get0_notAfter(cert); if (not_after == nullptr) { return false; } @@ -1677,7 +1673,12 @@ void SecureContext::Init(const FunctionCallbackInfo& args) { return THROW_ERR_CRYPTO_OPERATION_FAILED( env, "Error generating ticket keys"); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + SSL_CTX_set_tlsext_ticket_key_evp_cb(sc->ctx_.get(), + TicketCompatibilityCallback); +#else SSL_CTX_set_tlsext_ticket_key_cb(sc->ctx_.get(), TicketCompatibilityCallback); +#endif } SSLPointer SecureContext::CreateSSL() { @@ -2021,17 +2022,22 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo& args) { if (!bio) return; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVPKeyPointer params(PEM_read_bio_Parameters(bio.get(), nullptr)); + if (params && params.id() == EVP_PKEY_DH) dh.reset(params.release()); +#else dh.reset(PEM_read_bio_DHparams(bio.get(), nullptr, nullptr, nullptr)); +#endif } // Invalid dhparam is silently discarded and DHE is no longer used. // TODO(tniessen): don't silently discard invalid dhparam. + // TODO(panva): In a semver-major, reject non-DH parameter PEMs instead of + // silently treating them as absent. if (!dh) return; - const BIGNUM* p; - DH_get0_pqg(dh.get(), &p, nullptr, nullptr); - const int size = BignumPointer::GetBitCount(p); + const int size = dh.getPrimeBits(); if (size < 1024) { return THROW_ERR_INVALID_ARG_VALUE( env, "DH parameter is less than 1024 bits"); @@ -2040,10 +2046,18 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo& args) { env->isolate(), "DH parameter is less than 2048 bits")); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVPKeyPointer dh_pkey(dh.release()); + if (!SSL_CTX_set0_tmp_dh_pkey(sc->ctx_.get(), dh_pkey.get())) { +#else if (!SSL_CTX_set_tmp_dh(sc->ctx_.get(), dh.get())) { +#endif return THROW_ERR_CRYPTO_OPERATION_FAILED( env, "Error setting temp DH parameter"); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + dh_pkey.release(); +#endif } void SecureContext::SetMinProto(const FunctionCallbackInfo& args) { @@ -2404,7 +2418,7 @@ void SecureContext::SetClientCertEngine( } // Note that this takes another reference to `engine`. - if (!SSL_CTX_set_client_cert_engine(sc->ctx_.get(), engine.get())) + if (!engine.setClientCertEngine(sc->ctx_.get())) return ThrowCryptoError(env, ERR_get_error()); sc->client_cert_engine_provided_ = true; } @@ -2449,14 +2463,43 @@ void SecureContext::EnableTicketKeyCallback( SecureContext* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + SSL_CTX_set_tlsext_ticket_key_evp_cb(wrap->ctx_.get(), TicketKeyCallback); +#else SSL_CTX_set_tlsext_ticket_key_cb(wrap->ctx_.get(), TicketKeyCallback); +#endif +} + +namespace { +#if NCRYPTO_USE_OPENSSL3_PROVIDER +bool InitTicketHmac(EVP_MAC_CTX* hctx, + const unsigned char* key, + size_t key_len) { + const char* md_name = EVP_MD_get0_name(Digest::SHA256); + if (md_name == nullptr) return false; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, const_cast(md_name), 0), + OSSL_PARAM_construct_end(), + }; + return EVP_MAC_init(hctx, key, key_len, params) == 1; +} +#else +bool InitTicketHmac(HMAC_CTX* hctx, const unsigned char* key, size_t key_len) { + return HMAC_Init_ex(hctx, key, key_len, Digest::SHA256, nullptr) == 1; } +#endif +} // namespace int SecureContext::TicketKeyCallback(SSL* ssl, unsigned char* name, unsigned char* iv, EVP_CIPHER_CTX* ectx, +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_MAC_CTX* hctx, +#else HMAC_CTX* hctx, +#endif int enc) { static const int kTicketPartSize = 16; @@ -2529,8 +2572,9 @@ int SecureContext::TicketKeyCallback(SSL* ssl, } ArrayBufferViewContents hmac_buf(hmac); - HMAC_Init_ex( - hctx, hmac_buf.data(), hmac_buf.length(), Digest::SHA256, nullptr); + if (!InitTicketHmac(hctx, hmac_buf.data(), hmac_buf.length())) { + return -1; + } ArrayBufferViewContents aes_key(aes.As()); if (enc) { @@ -2546,7 +2590,11 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl, unsigned char* name, unsigned char* iv, EVP_CIPHER_CTX* ectx, +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_MAC_CTX* hctx, +#else HMAC_CTX* hctx, +#endif int enc) { SecureContext* sc = static_cast( SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))); @@ -2556,11 +2604,8 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl, if (!ncrypto::CSPRNG(iv, 16) || EVP_EncryptInit_ex( ectx, Cipher::AES_128_CBC, nullptr, sc->ticket_key_aes_, iv) <= 0 || - HMAC_Init_ex(hctx, - sc->ticket_key_hmac_, - sizeof(sc->ticket_key_hmac_), - Digest::SHA256, - nullptr) <= 0) { + !InitTicketHmac( + hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_))) { return -1; } return 1; @@ -2573,11 +2618,8 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl, if (EVP_DecryptInit_ex( ectx, Cipher::AES_128_CBC, nullptr, sc->ticket_key_aes_, iv) <= 0 || - HMAC_Init_ex(hctx, - sc->ticket_key_hmac_, - sizeof(sc->ticket_key_hmac_), - Digest::SHA256, - nullptr) <= 0) { + !InitTicketHmac( + hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_))) { return -1; } return 1; diff --git a/src/crypto/crypto_context.h b/src/crypto/crypto_context.h index ffb0b88816c092..95ddea4c262dd5 100644 --- a/src/crypto/crypto_context.h +++ b/src/crypto/crypto_context.h @@ -158,14 +158,22 @@ class SecureContext final : public BaseObject { unsigned char* name, unsigned char* iv, EVP_CIPHER_CTX* ectx, +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_MAC_CTX* hctx, +#else HMAC_CTX* hctx, +#endif int enc); static int TicketCompatibilityCallback(SSL* ssl, unsigned char* name, unsigned char* iv, EVP_CIPHER_CTX* ectx, +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_MAC_CTX* hctx, +#else HMAC_CTX* hctx, +#endif int enc); SecureContext(Environment* env, v8::Local wrap); diff --git a/src/crypto/crypto_dh.cc b/src/crypto/crypto_dh.cc index 10dfd055021737..2af9aacaefe460 100644 --- a/src/crypto/crypto_dh.cc +++ b/src/crypto/crypto_dh.cc @@ -89,6 +89,26 @@ MaybeLocal DataPointerToBuffer(Environment* env, DataPointer&& data) { return Buffer::New(env, ab, 0, ab->ByteLength()).FromMaybe(Local()); } +void PutDhError(int reason) { +#ifdef OPENSSL_IS_BORINGSSL + OPENSSL_PUT_ERROR(DH, reason); +#elif NCRYPTO_USE_OPENSSL3_PROVIDER + ERR_raise(ERR_LIB_DH, reason); +#else + ERR_put_error(ERR_LIB_DH, 0, reason, __FILE__, __LINE__); +#endif +} + +#if defined(OPENSSL_IS_BORINGSSL) || !NCRYPTO_USE_OPENSSL3_PROVIDER +void PutBnError(int reason) { +#ifdef OPENSSL_IS_BORINGSSL + OPENSSL_PUT_ERROR(BN, reason); +#else + ERR_put_error(ERR_LIB_BN, 0, reason, __FILE__, __LINE__); +#endif +} +#endif + void DiffieHellmanGroup(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 1); @@ -115,12 +135,12 @@ void New(const FunctionCallbackInfo& args) { if (bits < 2) { #ifndef OPENSSL_IS_BORINGSSL #if OPENSSL_VERSION_MAJOR >= 3 - ERR_put_error(ERR_LIB_DH, 0, DH_R_MODULUS_TOO_SMALL, __FILE__, __LINE__); + PutDhError(DH_R_MODULUS_TOO_SMALL); #else - ERR_put_error(ERR_LIB_BN, 0, BN_R_BITS_TOO_SMALL, __FILE__, __LINE__); + PutBnError(BN_R_BITS_TOO_SMALL); #endif // OPENSSL_VERSION_MAJOR >= 3 #else // OPENSSL_IS_BORINGSSL - OPENSSL_PUT_ERROR(BN, BN_R_BITS_TOO_SMALL); + PutBnError(BN_R_BITS_TOO_SMALL); #endif // OPENSSL_IS_BORINGSSL return ThrowCryptoError(env, ERR_get_error(), "Invalid prime length"); } @@ -134,11 +154,7 @@ void New(const FunctionCallbackInfo& args) { } int32_t generator = args[1].As()->Value(); if (generator < 2) { -#ifndef OPENSSL_IS_BORINGSSL - ERR_put_error(ERR_LIB_DH, 0, DH_R_BAD_GENERATOR, __FILE__, __LINE__); -#else - OPENSSL_PUT_ERROR(DH, DH_R_BAD_GENERATOR); -#endif + PutDhError(DH_R_BAD_GENERATOR); return ThrowCryptoError(env, ERR_get_error(), "Invalid generator"); } @@ -167,20 +183,12 @@ void New(const FunctionCallbackInfo& args) { if (args[1]->IsInt32()) { int32_t generator = args[1].As()->Value(); if (generator < 2) { -#ifndef OPENSSL_IS_BORINGSSL - ERR_put_error(ERR_LIB_DH, 0, DH_R_BAD_GENERATOR, __FILE__, __LINE__); -#else - OPENSSL_PUT_ERROR(DH, DH_R_BAD_GENERATOR); -#endif + PutDhError(DH_R_BAD_GENERATOR); return ThrowCryptoError(env, ERR_get_error(), "Invalid generator"); } bn_g = BignumPointer::New(); if (!bn_g.setWord(generator)) { -#ifndef OPENSSL_IS_BORINGSSL - ERR_put_error(ERR_LIB_DH, 0, DH_R_BAD_GENERATOR, __FILE__, __LINE__); -#else - OPENSSL_PUT_ERROR(DH, DH_R_BAD_GENERATOR); -#endif + PutDhError(DH_R_BAD_GENERATOR); return ThrowCryptoError(env, ERR_get_error(), "Invalid generator"); } } else { @@ -189,23 +197,22 @@ void New(const FunctionCallbackInfo& args) { return THROW_ERR_OUT_OF_RANGE(env, "generator is too big"); bn_g = BignumPointer(reinterpret_cast(arg1.data()), arg1.size()); if (!bn_g) { -#ifndef OPENSSL_IS_BORINGSSL - ERR_put_error(ERR_LIB_DH, 0, DH_R_BAD_GENERATOR, __FILE__, __LINE__); -#else - OPENSSL_PUT_ERROR(DH, DH_R_BAD_GENERATOR); -#endif + PutDhError(DH_R_BAD_GENERATOR); return ThrowCryptoError(env, ERR_get_error(), "Invalid generator"); } if (bn_g.getWord().has_value() && bn_g.getWord().value() < 2) { -#ifndef OPENSSL_IS_BORINGSSL - ERR_put_error(ERR_LIB_DH, 0, DH_R_BAD_GENERATOR, __FILE__, __LINE__); -#else - OPENSSL_PUT_ERROR(DH, DH_R_BAD_GENERATOR); -#endif + PutDhError(DH_R_BAD_GENERATOR); return ThrowCryptoError(env, ERR_get_error(), "Invalid generator"); } } +#ifndef OPENSSL_IS_BORINGSSL + if (BN_num_bits(bn_p.get()) >= 512 && BN_cmp(bn_g.get(), bn_p.get()) >= 0) { + PutDhError(DH_R_BAD_GENERATOR); + return ThrowCryptoError(env, ERR_get_error(), "Invalid generator"); + } +#endif + auto dh = DHPointer::New(std::move(bn_p), std::move(bn_g)); if (!dh) { return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid DH parameters"); diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index a89e3391dbf896..38f6831e7a9dee 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -187,15 +187,16 @@ void ECDH::ComputeSecret(const FunctionCallbackInfo& args) { return; } - int field_size = EC_GROUP_get_degree(ecdh->group_); - size_t out_len = (field_size + 7) / 8; - auto bs = ArrayBuffer::NewBackingStore( - env->isolate(), out_len, BackingStoreInitializationMode::kUninitialized); - - if (!ECDH_compute_key( - bs->Data(), bs->ByteLength(), pub, ecdh->key_.get(), nullptr)) + auto secret = ecdh->key_.computeSecret(pub); + if (!secret) return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to compute ECDH key"); + auto bs = ArrayBuffer::NewBackingStore( + env->isolate(), + secret.size(), + BackingStoreInitializationMode::kUninitialized); + memcpy(bs->Data(), secret.get(), secret.size()); + Local ab = ArrayBuffer::New(env->isolate(), std::move(bs)); Local buffer; if (!Buffer::New(env, ab, 0, ab->ByteLength()).ToLocal(&buffer)) return; @@ -469,11 +470,14 @@ bool ExportJWKEcKey(Environment* env, const auto& m_pkey = key.GetAsymmetricKey(); CHECK_EQ(m_pkey.id(), EVP_PKEY_EC); - const EC_KEY* ec = m_pkey; - CHECK_NOT_NULL(ec); + ECKeyPointer ec(m_pkey); + if (!ec) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); + return false; + } - const auto pub = ECKeyPointer::GetPublicKey(ec); - const auto group = ECKeyPointer::GetGroup(ec); + const auto pub = ec.getPublicKey(); + const auto group = ec.getGroup(); int degree_bits = EC_GROUP_get_degree(group); int degree_bytes = @@ -538,7 +542,7 @@ bool ExportJWKEcKey(Environment* env, } if (key.GetKeyType() == kKeyTypePrivate) { - auto pvt = ECKeyPointer::GetPrivateKey(ec); + auto pvt = ec.getPrivateKey(); return SetEncodedValue(env, target, env->jwk_d_string(), pvt, degree_bytes) .IsJust(); } @@ -751,11 +755,12 @@ bool GetEcKeyDetail(Environment* env, const auto& m_pkey = key.GetAsymmetricKey(); CHECK_EQ(m_pkey.id(), EVP_PKEY_EC); - const EC_KEY* ec = m_pkey; - CHECK_NOT_NULL(ec); + ECKeyPointer ec(m_pkey); + if (!ec) return true; - const auto group = ECKeyPointer::GetGroup(ec); + const auto group = ec.getGroup(); int nid = EC_GROUP_get_curve_name(group); + if (nid == NID_undef) return true; return target ->Set(env->context(), @@ -770,11 +775,12 @@ bool GetEcKeyDetail(Environment* env, // https://github.com/chromium/chromium/blob/7af6cfd/components/webcrypto/algorithms/ecdsa.cc size_t GroupOrderSize(const EVPKeyPointer& key) { - const EC_KEY* ec = key; - CHECK_NOT_NULL(ec); + ECKeyPointer ec(key); + if (!ec) return 0; auto order = BignumPointer::New(); - CHECK(order); - CHECK(EC_GROUP_get_order(ECKeyPointer::GetGroup(ec), order.get(), nullptr)); + if (!order || !EC_GROUP_get_order(ec.getGroup(), order.get(), nullptr)) { + return 0; + } return order.byteLength(); } } // namespace crypto diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 989f81302d198d..1a98b7b5b0c9ca 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -247,7 +247,11 @@ const EVP_MD* GetDigestImplementation(Environment* env, } void MarkInvalidXofLength() { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + ERR_raise(ERR_LIB_EVP, EVP_R_NOT_XOF_OR_INVALID_LENGTH); +#else EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH); +#endif } // DEP0198 EOL requires XOFs without an OpenSSL-defined default output length @@ -459,7 +463,7 @@ bool Hash::HashInit(const EVP_MD* md, Maybe xof_md_len) { // This is a little hack to cause createHash to fail when an incorrect // hashSize option was passed for a non-XOF hash function. if (!mdctx_.hasXofFlag()) [[unlikely]] { - EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH); + MarkInvalidXofLength(); mdctx_.reset(); return false; } diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 40b5ae9563ee7b..1404390f4bc8bc 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -385,11 +385,14 @@ bool KeyObjectData::ToEncodedPublicKey( Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); if (pkey.id() == EVP_PKEY_EC) { - const EC_KEY* ec_key = pkey; - CHECK_NOT_NULL(ec_key); + ECKeyPointer ec_key(pkey); + if (!ec_key) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; + } auto form = static_cast(config.ec_point_form); - const auto group = ECKeyPointer::GetGroup(ec_key); - const auto point = ECKeyPointer::GetPublicKey(ec_key); + const auto group = ec_key.getGroup(); + const auto point = ec_key.getPublicKey(); return ECPointToBuffer(env, group, point, form).ToLocal(out); } const int id = pkey.id(); @@ -431,14 +434,23 @@ bool KeyObjectData::ToEncodedPrivateKey( Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); if (pkey.id() == EVP_PKEY_EC) { - const EC_KEY* ec_key = pkey; - CHECK_NOT_NULL(ec_key); - const BIGNUM* private_key = ECKeyPointer::GetPrivateKey(ec_key); - CHECK_NOT_NULL(private_key); - const auto group = ECKeyPointer::GetGroup(ec_key); + ECKeyPointer ec_key(pkey); + if (!ec_key) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; + } + const BIGNUM* private_key = ec_key.getPrivateKey(); + if (private_key == nullptr) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get EC private key"); + return false; + } + const auto group = ec_key.getGroup(); auto order = BignumPointer::New(); - CHECK(order); - CHECK(EC_GROUP_get_order(group, order.get(), nullptr)); + if (!order || !EC_GROUP_get_order(group, order.get(), nullptr)) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to export EC private key"); + return false; + } auto buf = BignumPointer::EncodePadded(private_key, order.byteLength()); if (!buf) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, @@ -629,7 +641,9 @@ static KeyObjectData ImportRawKey(Environment* env, throw_invalid(); return {}; } +#if NCRYPTO_USE_LEGACY_KEY_TYPES eckey.release(); +#endif return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey)); } @@ -1460,15 +1474,17 @@ void KeyObjectHandle::ExportECPublicRaw( return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } - const EC_KEY* ec_key = m_pkey; - CHECK_NOT_NULL(ec_key); + ECKeyPointer ec_key(m_pkey); + if (!ec_key) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + } CHECK(args[0]->IsInt32()); auto form = static_cast(args[0].As()->Value()); - const auto group = ECKeyPointer::GetGroup(ec_key); - const auto point = ECKeyPointer::GetPublicKey(ec_key); + const auto group = ec_key.getGroup(); + const auto point = ec_key.getPublicKey(); Local buf; if (!ECPointToBuffer(env, group, point, form).ToLocal(&buf)) return; @@ -1491,16 +1507,23 @@ void KeyObjectHandle::ExportECPrivateRaw( return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } - const EC_KEY* ec_key = m_pkey; - CHECK_NOT_NULL(ec_key); + ECKeyPointer ec_key(m_pkey); + if (!ec_key) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + } - const BIGNUM* private_key = ECKeyPointer::GetPrivateKey(ec_key); - CHECK_NOT_NULL(private_key); + const BIGNUM* private_key = ec_key.getPrivateKey(); + if (private_key == nullptr) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to get EC private key"); + } - const auto group = ECKeyPointer::GetGroup(ec_key); + const auto group = ec_key.getGroup(); auto order = BignumPointer::New(); - CHECK(order); - CHECK(EC_GROUP_get_order(group, order.get(), nullptr)); + if (!order || !EC_GROUP_get_order(group, order.get(), nullptr)) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to export EC private key"); + } auto buf = BignumPointer::EncodePadded(private_key, order.byteLength()); if (!buf) { diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index acec4b993613cd..ad27108418353f 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -19,7 +19,9 @@ using ncrypto::DataPointer; using ncrypto::Digest; using ncrypto::EVPKeyCtxPointer; using ncrypto::EVPKeyPointer; +#if NCRYPTO_USE_LEGACY_KEY_TYPES using ncrypto::RSAPointer; +#endif using v8::ArrayBuffer; using v8::BackingStoreInitializationMode; using v8::FunctionCallbackInfo; @@ -353,6 +355,9 @@ KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { KeyType type = d_value->IsString() ? kKeyTypePrivate : kKeyTypePublic; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + ncrypto::Rsa rsa_view; +#else RSAPointer rsa(RSA_new()); if (!rsa) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Unable to create RSA pointer"); @@ -360,6 +365,7 @@ KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { } ncrypto::Rsa rsa_view(rsa.get()); +#endif ByteSource n = ByteSource::FromEncodedString(env, n_value.As()); ByteSource e = ByteSource::FromEncodedString(env, e_value.As()); @@ -421,7 +427,11 @@ KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { } } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + auto pkey = EVPKeyPointer::NewRSA(rsa_view); +#else auto pkey = EVPKeyPointer::NewRSA(std::move(rsa)); +#endif if (!pkey) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Unable to create key pointer"); return {}; diff --git a/src/crypto/crypto_tls.cc b/src/crypto/crypto_tls.cc index 28eb760dbb4cd6..ce44ad15fa79d3 100644 --- a/src/crypto/crypto_tls.cc +++ b/src/crypto/crypto_tls.cc @@ -874,7 +874,11 @@ void TLSWrap::ClearOut() { return; const char* ls = ERR_lib_error_string(ssl_err); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const char* fs = nullptr; +#else const char* fs = ERR_func_error_string(ssl_err); +#endif const char* rs = ERR_reason_error_string(ssl_err); if (!Set(env(), obj, env()->library_string(), ls) || !Set(env(), obj, env()->function_string(), fs) || diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 8d22350adffaac..711984e5e4f23f 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -12,10 +12,6 @@ #include "util-inl.h" #include "v8.h" -#ifndef OPENSSL_NO_ENGINE -#include -#endif // !OPENSSL_NO_ENGINE - #include "math.h" #if OPENSSL_VERSION_MAJOR >= 3 @@ -544,7 +540,11 @@ Maybe Decorate(Environment* env, if (err == 0) return JustVoid(); // No decoration necessary. const char* ls = ERR_lib_error_string(err); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const char* fs = nullptr; +#else const char* fs = ERR_func_error_string(err); +#endif const char* rs = ERR_reason_error_string(err); Isolate* isolate = env->isolate(); diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc index 7c680dc2c5eae9..6ed7d7d25db02e 100644 --- a/src/crypto/crypto_x509.cc +++ b/src/crypto/crypto_x509.cc @@ -20,7 +20,6 @@ using ncrypto::BIOPointer; using ncrypto::ClearErrorOnReturn; using ncrypto::DataPointer; using ncrypto::Digest; -using ncrypto::ECKeyPointer; using ncrypto::SSLPointer; using ncrypto::X509Name; using ncrypto::X509Pointer; @@ -668,17 +667,10 @@ static MaybeLocal GetX509NameObject(Environment* env, } MaybeLocal GetPubKey(Environment* env, const ncrypto::Rsa& rsa) { - int size = i2d_RSA_PUBKEY(rsa, nullptr); - CHECK_GE(size, 0); - - auto bs = ArrayBuffer::NewBackingStore( - env->isolate(), size, BackingStoreInitializationMode::kUninitialized); - - auto serialized = reinterpret_cast(bs->Data()); - CHECK_GE(i2d_RSA_PUBKEY(rsa, &serialized), 0); - - auto ab = ArrayBuffer::New(env->isolate(), std::move(bs)); - return Buffer::New(env, ab, 0, ab->ByteLength()).FromMaybe(Local()); + auto bio = rsa.derPublicKey(); + Local ret; + if (!ToBuffer(env, &bio).ToLocal(&ret)) return {}; + return ret.As(); } MaybeLocal GetModulusString(Environment* env, const BIGNUM* n) { @@ -698,14 +690,13 @@ MaybeLocal GetExponentString(Environment* env, const BIGNUM* e) { return ToV8Value(env->context(), bio); } -MaybeLocal GetECPubKey(Environment* env, - const EC_GROUP* group, - OSSL3_CONST EC_KEY* ec) { - const auto pubkey = ECKeyPointer::GetPublicKey(ec); +MaybeLocal GetECPubKey(Environment* env, const ncrypto::Ec& ec) { + const auto group = ec.getGroup(); + const auto pubkey = ec.getPublicKey(); if (pubkey == nullptr) [[unlikely]] return Undefined(env->isolate()); - return ECPointToBuffer(env, group, pubkey, EC_KEY_get_conv_form(ec)) + return ECPointToBuffer(env, group, pubkey, ec.getPointConversionForm()) .FromMaybe(Local()); } @@ -792,7 +783,7 @@ MaybeLocal X509ToObject(Environment* env, const X509View& cert) { cert.ifEc([&](const ncrypto::Ec& ec) { const auto group = ec.getGroup(); - values[7] = GetECPubKey(env, group, ec); // pubkey + values[7] = GetECPubKey(env, ec); // pubkey values[8] = GetECGroupBits(env, group); // bits const int nid = ec.getCurve(); if (nid != 0) { diff --git a/src/node.cc b/src/node.cc index 4ba019ddca05f4..b368c58734345f 100644 --- a/src/node.cc +++ b/src/node.cc @@ -50,6 +50,11 @@ #if HAVE_OPENSSL #include "ncrypto.h" #include "node_crypto.h" +#if OPENSSL_VERSION_MAJOR >= 3 && !defined(CONF_MFLAGS_IGNORE_MISSING_FILE) +// OpenSSL hides this deprecated macro under OPENSSL_NO_DEPRECATED, but the +// non-deprecated OPENSSL_INIT settings API still accepts the flag value. +#define CONF_MFLAGS_IGNORE_MISSING_FILE 0x10 +#endif #endif #if defined(NODE_HAVE_I18N_SUPPORT) diff --git a/src/node_constants.cc b/src/node_constants.cc index 50ece35429e932..8abf3fd8afe483 100644 --- a/src/node_constants.cc +++ b/src/node_constants.cc @@ -39,8 +39,58 @@ #if HAVE_OPENSSL #include #include +#if !defined(RSA_PKCS1_PADDING) +#define RSA_PKCS1_PADDING 1 +#endif +#if !defined(RSA_SSLV23_PADDING) +#define RSA_SSLV23_PADDING 2 +#endif +#if !defined(RSA_NO_PADDING) +#define RSA_NO_PADDING 3 +#endif +#if !defined(RSA_PKCS1_OAEP_PADDING) +#define RSA_PKCS1_OAEP_PADDING 4 +#endif +#if !defined(RSA_X931_PADDING) +#define RSA_X931_PADDING 5 +#endif +#if !defined(RSA_PKCS1_PSS_PADDING) +#define RSA_PKCS1_PSS_PADDING 6 +#endif +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_MAJOR >= 3 +// OpenSSL hides these deprecated DH check constants under +// OPENSSL_NO_DEPRECATED, but the numeric verifyError values remain public API. +#if !defined(DH_CHECK_P_NOT_PRIME) +#define DH_CHECK_P_NOT_PRIME 0x01 +#endif +#if !defined(DH_CHECK_P_NOT_SAFE_PRIME) +#define DH_CHECK_P_NOT_SAFE_PRIME 0x02 +#endif +#if !defined(DH_UNABLE_TO_CHECK_GENERATOR) +#define DH_UNABLE_TO_CHECK_GENERATOR 0x04 +#endif +#if !defined(DH_NOT_SUITABLE_GENERATOR) +#define DH_NOT_SUITABLE_GENERATOR 0x08 +#endif +#endif #ifndef OPENSSL_NO_ENGINE +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_MAJOR >= 3 +// Engine constants remain public API while engine implementation lives in the +// dedicated compatibility target. +#define ENGINE_METHOD_RSA (unsigned int)0x0001 +#define ENGINE_METHOD_DSA (unsigned int)0x0002 +#define ENGINE_METHOD_DH (unsigned int)0x0004 +#define ENGINE_METHOD_RAND (unsigned int)0x0008 +#define ENGINE_METHOD_CIPHERS (unsigned int)0x0040 +#define ENGINE_METHOD_DIGESTS (unsigned int)0x0080 +#define ENGINE_METHOD_PKEY_METHS (unsigned int)0x0200 +#define ENGINE_METHOD_PKEY_ASN1_METHS (unsigned int)0x0400 +#define ENGINE_METHOD_EC (unsigned int)0x0800 +#define ENGINE_METHOD_ALL (unsigned int)0xFFFF +#define ENGINE_METHOD_NONE (unsigned int)0x0000 +#else #include +#endif #endif // !OPENSSL_NO_ENGINE #endif // HAVE_OPENSSL diff --git a/src/node_contextify.cc b/src/node_contextify.cc index dcfaf02c7e55f3..0d1e7efe908f6d 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -200,16 +200,18 @@ void ContextifyContext::InitializeGlobalTemplates(IsolateData* isolate_data) { Local global_object_template = global_func_template->InstanceTemplate(); - NamedPropertyHandlerConfiguration config( - PropertyGetterCallback, - PropertySetterCallback, - PropertyQueryCallback, - PropertyDeleterCallback, - PropertyEnumeratorCallback, - PropertyDefinerCallback, - PropertyDescriptorCallback, - {}, - PropertyHandlerFlags::kHasNoSideEffect); + PropertyHandlerFlags flags = static_cast( + static_cast(PropertyHandlerFlags::kHasNoSideEffect) | + static_cast(PropertyHandlerFlags::kHasDontDeleteProperty)); + NamedPropertyHandlerConfiguration config(PropertyGetterCallback, + PropertySetterCallback, + PropertyQueryCallback, + PropertyDeleterCallback, + PropertyEnumeratorCallback, + PropertyDefinerCallback, + PropertyDescriptorCallback, + {}, + flags); IndexedPropertyHandlerConfiguration indexed_config( IndexedPropertyGetterCallback, @@ -220,7 +222,7 @@ void ContextifyContext::InitializeGlobalTemplates(IsolateData* isolate_data) { IndexedPropertyDefinerCallback, IndexedPropertyDescriptorCallback, {}, - PropertyHandlerFlags::kHasNoSideEffect); + flags); global_object_template->SetHandler(config); global_object_template->SetHandler(indexed_config); diff --git a/src/node_ffi.cc b/src/node_ffi.cc index a5b51cb05c0692..8e5729cb4ee0d8 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -1215,6 +1215,17 @@ Local DynamicLibrary::GetConstructorTemplate( return tmpl; } +void GetCurrentEventLoop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + + args.GetReturnValue().Set(BigInt::NewFromUnsigned( + isolate, + static_cast(reinterpret_cast(env->event_loop())))); +} + // Module initialization. static void Initialize(Local target, Local unused, @@ -1230,6 +1241,7 @@ static void Initialize(Local target, SetMethod(context, target, "toArrayBuffer", ToArrayBuffer); SetMethod(context, target, "exportBytes", ExportBytes); SetMethod(context, target, "getRawPointer", GetRawPointer); + SetMethod(context, target, "getCurrentEventLoop", GetCurrentEventLoop); SetMethod(context, target, "getInt8", GetInt8); SetMethod(context, target, "getUint8", GetUint8); diff --git a/src/node_ffi.h b/src/node_ffi.h index dc44405121371e..a55cb74fc619e9 100644 --- a/src/node_ffi.h +++ b/src/node_ffi.h @@ -178,6 +178,7 @@ void ToBuffer(const v8::FunctionCallbackInfo& args); void ToArrayBuffer(const v8::FunctionCallbackInfo& args); void ExportBytes(const v8::FunctionCallbackInfo& args); void GetRawPointer(const v8::FunctionCallbackInfo& args); +void GetCurrentEventLoop(const v8::FunctionCallbackInfo& args); } // namespace node::ffi diff --git a/src/node_http2.cc b/src/node_http2.cc index fca840c0fa03ad..c4bd4db6071d97 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -2308,6 +2308,8 @@ Http2Stream::Http2Stream(Http2Session* session, if (options & STREAM_OPTION_GET_TRAILERS) set_has_trailers(); + if (options & STREAM_OPTION_AUTO_EMPTY_TRAILERS) set_auto_empty_trailers(); + PushStreamListener(&stream_listener_); if (options & STREAM_OPTION_EMPTY_PAYLOAD) @@ -2435,6 +2437,8 @@ int Http2Stream::SubmitResponse(const Http2Headers& headers, int options) { if (options & STREAM_OPTION_GET_TRAILERS) set_has_trailers(); + if (options & STREAM_OPTION_AUTO_EMPTY_TRAILERS) set_auto_empty_trailers(); + if (!is_writable()) options |= STREAM_OPTION_EMPTY_PAYLOAD; @@ -2468,39 +2472,60 @@ int Http2Stream::SubmitInfo(const Http2Headers& headers) { } void Http2Stream::OnTrailers() { + CHECK(!this->is_destroyed()); + set_has_trailers(false); + if (!auto_empty_trailers()) { + EmitWantTrailers(); + return; + } + // The JS side has not registered any trailers, so the stream can be + // finished without calling into JS at all. The empty DATA frame cannot be + // submitted synchronously because OnTrailers() runs from inside the data + // source read callback for the final DATA frame; defer it to the next + // turn of the event loop, just like the JS sendTrailers() path does. + Debug(this, "auto-submitting empty trailers"); + env()->SetImmediate( + [self = BaseObjectPtr(this)](Environment* env) { + if (self->is_destroyed()) return; + // Hand control back to the JS side if trailers were registered in + // the meantime or if submitting the empty DATA frame failed. + if (!self->auto_empty_trailers() || self->SubmitEmptyTrailers() != 0) + self->EmitWantTrailers(); + }); +} + +void Http2Stream::EmitWantTrailers() { Debug(this, "let javascript know we are ready for trailers"); CHECK(!this->is_destroyed()); Isolate* isolate = env()->isolate(); HandleScope scope(isolate); Local context = env()->context(); Context::Scope context_scope(context); - set_has_trailers(false); MakeCallback(env()->http2session_on_stream_trailers_function(), 0, nullptr); } -// Submit informational headers for a stream. +// Sending an empty trailers frame poses problems in Safari, Edge & IE. +// Instead we can just send an empty data frame with NGHTTP2_FLAG_END_STREAM +// to indicate that the stream is ready to be closed. +int Http2Stream::SubmitEmptyTrailers() { + CHECK(!this->is_destroyed()); + Http2Scope h2scope(this); + Debug(this, "sending empty trailers"); + Http2Stream::Provider::Stream prov(this, 0); + int ret = nghttp2_submit_data( + session_->session(), NGHTTP2_FLAG_END_STREAM, id_, *prov); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + +// Submit trailing headers for a stream. int Http2Stream::SubmitTrailers(const Http2Headers& headers) { CHECK(!this->is_destroyed()); + if (headers.length() == 0) return SubmitEmptyTrailers(); Http2Scope h2scope(this); Debug(this, "sending %d trailers", headers.length()); - int ret; - // Sending an empty trailers frame poses problems in Safari, Edge & IE. - // Instead we can just send an empty data frame with NGHTTP2_FLAG_END_STREAM - // to indicate that the stream is ready to be closed. - if (headers.length() == 0) { - Http2Stream::Provider::Stream prov(this, 0); - ret = nghttp2_submit_data( - session_->session(), - NGHTTP2_FLAG_END_STREAM, - id_, - *prov); - } else { - ret = nghttp2_submit_trailer( - session_->session(), - id_, - headers.data(), - headers.length()); - } + int ret = nghttp2_submit_trailer( + session_->session(), id_, headers.data(), headers.length()); CHECK_NE(ret, NGHTTP2_ERR_NOMEM); return ret; } @@ -3110,6 +3135,15 @@ void Http2Stream::Trailers(const FunctionCallbackInfo& args) { stream->SubmitTrailers(Http2Headers(env, headers))); } +// Called by the JS layer when trailers are registered after the response +// headers were already submitted with STREAM_OPTION_AUTO_EMPTY_TRAILERS set, +// so that the trailers are handed back to JS instead of being auto-emptied. +void Http2Stream::DisableAutoTrailers(const FunctionCallbackInfo& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); + stream->set_auto_empty_trailers(false); +} + // Grab the numeric id of the Http2Stream void Http2Stream::GetID(const FunctionCallbackInfo& args) { Http2Stream* stream; @@ -3547,6 +3581,8 @@ void Initialize(Local target, SetProtoMethod(isolate, stream, "pushPromise", Http2Stream::PushPromise); SetProtoMethod(isolate, stream, "info", Http2Stream::Info); SetProtoMethod(isolate, stream, "trailers", Http2Stream::Trailers); + SetProtoMethod( + isolate, stream, "disableAutoTrailers", Http2Stream::DisableAutoTrailers); SetProtoMethod(isolate, stream, "respond", Http2Stream::Respond); SetProtoMethod(isolate, stream, "rstStream", Http2Stream::RstStream); SetProtoMethod(isolate, stream, "refreshState", Http2Stream::RefreshState); diff --git a/src/node_http2.h b/src/node_http2.h index c9957cb559b323..670c5b5db81f70 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -57,6 +57,10 @@ constexpr int STREAM_OPTION_EMPTY_PAYLOAD = 0x1; // Stream might have trailing headers constexpr int STREAM_OPTION_GET_TRAILERS = 0x2; +// Stream may finish with an empty DATA frame carrying END_STREAM without +// calling back into JS, unless trailers are registered before then +constexpr int STREAM_OPTION_AUTO_EMPTY_TRAILERS = 0x4; + // Http2Stream internal states constexpr int kStreamStateNone = 0x0; constexpr int kStreamStateShut = 0x1; @@ -66,6 +70,7 @@ constexpr int kStreamStateClosed = 0x8; constexpr int kStreamStateDestroyed = 0x10; constexpr int kStreamStateTrailers = 0x20; constexpr int kStreamStatePeerReset = 0x40; +constexpr int kStreamStateAutoEmptyTrailers = 0x80; // Http2Session internal states constexpr int kSessionStateNone = 0x0; @@ -310,7 +315,9 @@ class Http2Stream : public AsyncWrap, // Submit trailing headers for this stream int SubmitTrailers(const Http2Headers& headers); + int SubmitEmptyTrailers(); void OnTrailers(); + void EmitWantTrailers(); // Submit a PRIORITY frame for this stream int SubmitPriority(const Http2Priority& priority, bool silent = false); @@ -368,6 +375,17 @@ class Http2Stream : public AsyncWrap, flags_ &= ~kStreamStateTrailers; } + bool auto_empty_trailers() const { + return flags_ & kStreamStateAutoEmptyTrailers; + } + + void set_auto_empty_trailers(bool on = true) { + if (on) + flags_ |= kStreamStateAutoEmptyTrailers; + else + flags_ &= ~kStreamStateAutoEmptyTrailers; + } + void set_closed() { flags_ |= kStreamStateClosed; } @@ -463,6 +481,8 @@ class Http2Stream : public AsyncWrap, static void RefreshState(const v8::FunctionCallbackInfo& args); static void Info(const v8::FunctionCallbackInfo& args); static void Trailers(const v8::FunctionCallbackInfo& args); + static void DisableAutoTrailers( + const v8::FunctionCallbackInfo& args); static void Respond(const v8::FunctionCallbackInfo& args); static void RstStream(const v8::FunctionCallbackInfo& args); @@ -1119,7 +1139,8 @@ class Origins { V(NGHTTP2_ERR_STREAM_CLOSED) \ V(NGHTTP2_ERR_NOMEM) \ V(STREAM_OPTION_EMPTY_PAYLOAD) \ - V(STREAM_OPTION_GET_TRAILERS) + V(STREAM_OPTION_GET_TRAILERS) \ + V(STREAM_OPTION_AUTO_EMPTY_TRAILERS) #define HTTP2_ERROR_CODES(V) \ V(NGHTTP2_NO_ERROR) \ diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 049fd786126fce..c3b7d279f4dca0 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2865,12 +2865,16 @@ MaybeLocal StatementExecutionHelper::All(Environment* env, Isolate* isolate = env->isolate(); EscapableHandleScope scope(isolate); int r; - int num_cols = sqlite3_column_count(stmt); + int num_cols = 0; LocalVector rows(isolate); LocalVector row_values(isolate); LocalVector row_keys(isolate); while ((r = sqlite3_step(stmt)) == SQLITE_ROW) { + if (num_cols == 0) { + num_cols = sqlite3_column_count(stmt); + } + if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values) .IsNothing()) { return MaybeLocal(); diff --git a/src/quic/README.md b/src/quic/README.md index 9583acbcd76417..3d90cacdbb6c34 100644 --- a/src/quic/README.md +++ b/src/quic/README.md @@ -77,7 +77,7 @@ data channels that carry application data. Every entry point that may generate outbound data creates a `SendPendingDataScope`. Scopes nest — an internal depth counter ensures -`Application::SendPendingData()` is called exactly once, when the outermost +`Session::SendPendingData()` is called exactly once, when the outermost scope exits: ```cpp @@ -218,13 +218,13 @@ Session::Receive() ```text SendPendingDataScope::~SendPendingDataScope() - → Application::SendPendingData() + → Session::SendPendingData() Loop (up to max_packet_count): - ├── GetStreamData() // pull data from next stream - │ └── stream->Pull() // bob pull from Outbound→DataQueue - ├── WriteVStream() // ngtcp2_conn_writev_stream() + ├── application().GetStreamData() // pull data from next stream + │ └── stream->Pull() // bob pull from Outbound→DataQueue + ├── WriteVStream() // ngtcp2_conn_writev_stream() │ encrypts, frames, paces - ├── if ndatalen > 0: StreamCommit() + ├── if ndatalen > 0: application().StreamCommit() │ stream->Commit(datalen, fin) ├── if nwrite > 0: Send() // uv_udp_send() ├── if WRITE_MORE: continue // room for more in this packet diff --git a/src/quic/application.cc b/src/quic/application.cc index ce5d5e12154d8a..688a1309dea552 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -165,24 +165,6 @@ MaybeLocal Session::Application_Options::ToObject( // ============================================================================ -std::string Session::Application::StreamData::ToString() const { - DebugIndentScope indent; - - size_t total_bytes = 0; - for (size_t n = 0; n < count; n++) { - total_bytes += data[n].len; - } - - auto prefix = indent.Prefix(); - std::string res("{"); - res += prefix + "count: " + std::to_string(count); - res += prefix + "id: " + std::to_string(id); - res += prefix + "fin: " + std::to_string(fin); - res += prefix + "total: " + std::to_string(total_bytes); - res += indent.Close(); - return res; -} - Session::Application::Application(Session* session, const Options& options) : session_(session) {} @@ -254,11 +236,6 @@ bool Session::Application::ValidateTicketData( return true; } -Packet::Ptr Session::Application::CreateStreamDataPacket() { - return session_->endpoint().CreatePacket( - session_->remote_address(), session_->max_packet_size(), "stream data"); -} - void Session::Application::ReceiveStreamClose(Stream* stream, QuicError&& error) { DCHECK_NOT_NULL(stream); @@ -277,425 +254,6 @@ void Session::Application::ReceiveStreamReset(Stream* stream, stream->ReceiveStreamReset(final_size, std::move(error)); } -// Attempts to pack a pending datagram into the current packet. -// Returns the nwrite value from ngtcp2_conn_writev_datagram. -// On fatal error, closes the session and returns the error code. -// The caller should check: -// > 0: packet is complete, send it (pos was NOT advanced — caller -// must add nwrite to pos and send) -// NGTCP2_ERR_WRITE_MORE: datagram packed, room for more -// 0: congestion controlled or doesn't fit, datagram stays in queue -// < 0 (other): fatal error, session already closed -ssize_t Session::Application::TryWritePendingDatagram(PathStorage* path, - uint8_t* dest, - size_t destlen, - uint64_t ts) { - CHECK(session_->HasPendingDatagrams()); - auto max_attempts = session_->config().options.max_datagram_send_attempts; - - // Skip datagrams that have already exceeded the send attempt limit - // from a previous SendPendingData cycle. - while (session_->HasPendingDatagrams()) { - auto& front = session_->PeekPendingDatagram(); - if (front.send_attempts < max_attempts) break; - Debug(session_, - "Datagram %" PRIu64 " abandoned after %u attempts", - front.id, - front.send_attempts); - session_->DatagramStatus(front.id, DatagramStatus::ABANDONED); - session_->PopPendingDatagram(); - } - - if (!session_->HasPendingDatagrams()) return 0; - auto& dg = session_->PeekPendingDatagram(); - ngtcp2_vec dgvec = dg.data; - int accepted = 0; - int dg_flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; - - // PacketInfo for the datagram path. When libuv gains per-socket ECN - // marking, the value from ngtcp2 should be forwarded to the send path. - PacketInfo dg_pi; - ssize_t dg_nwrite = ngtcp2_conn_writev_datagram(*session_, - &path->path, - dg_pi, - dest, - destlen, - &accepted, - dg_flags, - dg.id, - &dgvec, - 1, - ts); - - if (accepted) { - // Nice, the datagram was accepted! - Debug(session_, "Datagram %" PRIu64 " accepted into packet", dg.id); - session_->DatagramSent(dg.id); - session_->PopPendingDatagram(); - } else { - Debug(session_, "Datagram %" PRIu64 " not accepted into packet", dg.id); - } - - switch (dg_nwrite) { - case 0: { - // If dg_nwrite is 0, we are either congestion controlled or - // there wasn't enough room in the packet for the datagram or - // we aren't in a state where we can send. - // We'll skip this attempt and return 0. - CHECK(!accepted); - dg.send_attempts++; - return 0; - } - case NGTCP2_ERR_WRITE_MORE: { - // There's still room left in the packet! - return NGTCP2_ERR_WRITE_MORE; - } - case NGTCP2_ERR_INVALID_STATE: - case NGTCP2_ERR_INVALID_ARGUMENT: { - // Non-fatal error cases. Peer either does not support datagrams - // or the datagram is too large for the peer's max. - // Abandon the datagram and signal skip by returning std::nullopt. - session_->DatagramStatus(dg.id, DatagramStatus::ABANDONED); - session_->PopPendingDatagram(); - return 0; - } - default: { - // Fatal errors: PKT_NUM_EXHAUSTED, CALLBACK_FAILURE, NOMEM, etc. - Debug(session_, "Fatal datagram error: %zd", dg_nwrite); - session_->SetLastError(QuicError::ForNgtcp2Error(dg_nwrite)); - session_->Close(CloseMethod::SILENT); - return dg_nwrite; - } - } - UNREACHABLE(); -} - -// the SendPendingData method is the primary driver for sending data from the -// application layer. It loops through available stream data and pending -// datagrams and generates packets to send until there is either no more -// data to send or we hit the maximum number of packets to send in one go. -// This method is extremely delicate. A bug in this method can break the -// entire QUIC implementation; so be very careful when making changes here -// and make sure to test thoroughly. When in doubt... don't change it. -void Session::Application::SendPendingData() { - DCHECK(!session().is_destroyed()); - if (!session().can_send_packets()) [[unlikely]] { - return; - } - // Upper bound on packets per SendPendingData call. ngtcp2's send quantum - // is typically 64 KB, which at 1200-byte minimum packet size is ~53 - // packets. 64 covers the worst case with headroom. The actual count per - // call is dynamically capped by ngtcp2_conn_get_send_quantum(). - static constexpr size_t kMaxPackets = 64; - Debug(session_, "Application sending pending data"); - // Cache the timestamp once for the entire send loop. ngtcp2 does not - // require nanosecond-accurate monotonicity within a single burst — - // a single timestamp per SendPendingData call is what other QUIC - // implementations use (e.g., quiche, msquic). When kernel-level - // packet pacing becomes available via libuv, this timestamp becomes - // the base for computing per-packet transmit timestamps. - const uint64_t ts = uv_hrtime(); - PathStorage path; - StreamData stream_data; - - bool closed = false; - - // Batch accumulation: packets are collected here and flushed via - // Session::SendBatch when the loop exits, the batch is full, or - // on early return. This enables synchronous batched delivery via - // uv_udp_try_send2 (sendmmsg) from the deferred flush path. - Packet::Ptr batch[kMaxPackets]; - PathStorage batch_paths[kMaxPackets]; - size_t batch_count = 0; - - auto flush_batch = [&] { - if (batch_count == 0) return; - session_->SendBatch(batch, batch_paths, batch_count); - batch_count = 0; - }; - - auto update_stats = OnScopeLeave([&] { - if (closed) return; - // Flush any remaining accumulated packets before updating stats. - flush_batch(); - if (session().is_destroyed()) [[unlikely]] - return; - - // Get a strong pointer to protect against potential destruction during - // updating the time and data stats. - BaseObjectPtr s(session_); - s->UpdatePacketTxTime(); - s->UpdateTimer(); - s->UpdateDataStats(); - }); - - // The maximum size of packet to create. - const size_t max_packet_size = session_->max_packet_size(); - - // The maximum number of packets to send in this call to SendPendingData. - const size_t max_packet_count = std::min( - kMaxPackets, ngtcp2_conn_get_send_quantum(*session_) / max_packet_size); - if (max_packet_count == 0) return; - - // The number of packets that have been prepared in this call. - size_t packet_send_count = 0; - - Packet::Ptr packet; - - auto ensure_packet = [&] { - if (!packet) { - packet = CreateStreamDataPacket(); - if (!packet) [[unlikely]] - return false; - } - DCHECK(packet); - return true; - }; - - // Accumulate a completed packet into the batch. - auto enqueue_packet = - [&](Packet::Ptr& pkt, size_t len, const PacketInfo& pi) { - Debug(session_, "Enqueuing packet with %zu bytes into batch", len); - pkt->Truncate(len); - pkt->set_pkt_info(pi); - path.CopyTo(&batch_paths[batch_count]); - batch[batch_count++] = std::move(pkt); - }; - - // We're going to enter a loop here to prepare and send no more than - // max_packet_count packets. - for (;;) { - // ndatalen is the amount of stream data that was accepted into the packet. - ssize_t ndatalen = 0; - - // Make sure we have a packet to write data into. - if (!ensure_packet()) [[unlikely]] { - Debug(session_, "Failed to create packet for stream data"); - // Doh! Could not create a packet. Time to bail. - session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - closed = true; - return session_->Close(CloseMethod::SILENT); - } - - // The stream_data is the next block of data from the application stream. - if (GetStreamData(&stream_data) < 0) { - Debug(session_, "Application failed to get stream data"); - session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - closed = true; - return session_->Close(CloseMethod::SILENT); - } - - // If we got here, we were at least successful in checking for stream data. - // There might not be any stream data to send. If there is no stream data, - // that's perfectly fine, we still need to serialize any frames we do have - // (pings, acks, datagrams, etc) so we'll just keep going. - if (stream_data.id >= 0) { - Debug(session_, "Application using stream data: %s", stream_data); - } else { - Debug(session_, "No stream data to send"); - } - if (session_->HasPendingDatagrams()) { - Debug(session_, "There are pending datagrams to send"); - } - - // Awesome, let's write our packet! - PacketInfo pi; - ssize_t nwrite = WriteVStream(&path, - &pi, - packet->data(), - &ndatalen, - packet->length(), - stream_data, - ts); - - // When ndatalen is > 0, that's our indication that stream data was accepted - // in to the packet. Yay! - if (ndatalen > 0) { - Debug(session_, - "Application accepted %zu bytes from stream %" PRIi64 - " into packet", - ndatalen, - stream_data.id); - if (!StreamCommit(&stream_data, ndatalen)) { - // Data was accepted into the packet, but for some reason adjusting - // the stream's committed data failed. Treat as fatal. - Debug(session_, "Failed to commit accepted bytes in stream"); - session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - closed = true; - return session_->Close(CloseMethod::SILENT); - } - } else if (stream_data.id >= 0) { - Debug(session_, - "Application did not accept any bytes from stream %" PRIi64 - " into packet", - stream_data.id); - } - - // When nwrite is zero, it means we are congestion limited or it is - // just not our turn to send something. Re-schedule the stream if it - // had unsent data (payload or FIN) so the next timer-triggered - // SendPendingData retries it. Without this, a FIN-only send that - // hits nwrite=0 is lost forever — the stream already returned EOS - // from Pull and won't be re-scheduled by anyone else. - // We call Application::ResumeStream directly (not Session::ResumeStream) - // to avoid creating a SendPendingDataScope — we're already inside - // SendPendingData and re-entering would just hit nwrite=0 again. - if (nwrite == 0) { - Debug(session_, "Congestion or not our turn to send"); - if (stream_data.id >= 0 && (stream_data.count > 0 || stream_data.fin)) { - ResumeStream(stream_data.id); - } - return; - } - - // A negative nwrite value indicates either an error or that there is more - // data to write into the packet. - if (nwrite < 0) { - switch (nwrite) { - case NGTCP2_ERR_STREAM_DATA_BLOCKED: { - // We could not write any data for this stream into the packet because - // the flow control for the stream itself indicates that the stream - // is blocked. We'll skip and move on to the next stream. - // ndatalen = -1 means that no stream data was accepted into the - // packet, which is what we want here. - DCHECK_EQ(ndatalen, -1); - // We should only have received this error if there was an actual - // stream identified in the stream data, but let's double check. - DCHECK_GE(stream_data.id, 0); - session_->StreamDataBlocked(stream_data.id); - continue; - } - case NGTCP2_ERR_STREAM_SHUT_WR: { - // Indicates that the writable side of the stream should be closed - // locally or the stream is being reset. In either case, we can't send - // data for this stream! - Debug(session_, - "Closing stream %" PRIi64 " for writing", - stream_data.id); - // ndatalen = -1 means that no stream data was accepted into the - // packet, which is what we want here. - DCHECK_EQ(ndatalen, -1); - // We should only have received this error if there was an actual - // stream identified in the stream data, but let's double check. - DCHECK_GE(stream_data.id, 0); - if (stream_data.stream) [[likely]] { - stream_data.stream->EndWritable(); - } - // Notify the application that the stream's write side is shut - // so it stops queuing data. Without this, GetStreamData would - // keep returning the same stream and we'd loop forever. - StreamWriteShut(stream_data.id); - continue; - } - case NGTCP2_ERR_WRITE_MORE: { - Debug(session_, "Packet buffer not full, coalesce more data into it"); - // Room for more in this packet. Try to pack a pending datagram - // if there is one. Otherwise just loop around and keep going. - if (session_->HasPendingDatagrams()) { - auto result = TryWritePendingDatagram( - &path, packet->data(), packet->length(), ts); - // When result is 0, either the datagram was congestion controlled, - // didn't fit in the packet, or was abandoned. Skip and continue. - - // When result is > 0, the packet is done and the result is the - // completed size of the packet we're sending. - if (result > 0) { - size_t len = result; - Debug(session_, "Sending packet with %zu bytes", len); - enqueue_packet(packet, len, pi); - if (++packet_send_count == max_packet_count) return; - } else if (result < 0) { - // Any negative result other than NGTCP2_ERR_WRITE_MORE - // at this point is fatal. The session will have been - // closed. - if (result != NGTCP2_ERR_WRITE_MORE) return; - } - } - continue; - } - case NGTCP2_ERR_CALLBACK_FAILURE: { - // This case really should not happen. It indicates that the - // ngtcp2 callback failed for some reason. This would be a - // bug in our code. - Debug(session_, "Internal failure with ngtcp2 callback"); - session_->SetLastError( - QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - closed = true; - return session_->Close(CloseMethod::SILENT); - } - } - - // Some other type of error happened. - DCHECK_EQ(ndatalen, -1); - Debug(session_, - "Application encountered error while writing packet: %s", - ngtcp2_strerror(nwrite)); - session_->SetLastError(QuicError::ForNgtcp2Error(nwrite)); - closed = true; - return session_->Close(CloseMethod::SILENT); - } - - // At this point we have a packet prepared to send. The nwrite - // is the size of the packet we are sending. - size_t len = nwrite; - Debug(session_, "Sending packet with %zu bytes", len); - enqueue_packet(packet, len, pi); - if (++packet_send_count == max_packet_count) return; - - // If there are pending datagrams, try sending them in a fresh packet. - // This is necessary because ngtcp2_conn_writev_stream only returns - // NGTCP2_ERR_WRITE_MORE when there is actual stream data — when no - // streams are active, the coalescing path above is never reached and - // datagrams would never be sent. - if (session_->HasPendingDatagrams()) { - if (!ensure_packet()) [[unlikely]] { - Debug(session_, "Failed to create packet for datagram"); - session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); - closed = true; - return session_->Close(CloseMethod::SILENT); - } - auto result = - TryWritePendingDatagram(&path, packet->data(), packet->length(), ts); - if (result > 0) { - Debug(session_, "Sending datagram packet with %zd bytes", result); - enqueue_packet(packet, static_cast(result), PacketInfo()); - if (++packet_send_count == max_packet_count) return; - } else if (result < 0 && result != NGTCP2_ERR_WRITE_MORE) { - // Fatal error — session already closed by TryWritePendingDatagram. - return; - } - // If result == 0 (congestion) or NGTCP2_ERR_WRITE_MORE (datagram - // packed but room for more), the loop continues normally. - } - } -} - -ssize_t Session::Application::WriteVStream(PathStorage* path, - PacketInfo* pi, - uint8_t* dest, - ssize_t* ndatalen, - size_t max_packet_size, - const StreamData& stream_data, - uint64_t ts) { - DCHECK_LE(stream_data.count, kMaxVectorCount); - uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; - if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; - // The PacketInfo out-param is populated by ngtcp2 with the ECN codepoint - // to apply when sending this packet. When libuv gains per-socket ECN - // marking, the value should be forwarded to the send path. - return ngtcp2_conn_writev_stream(*session_, - &path->path, - *pi, - dest, - max_packet_size, - ndatalen, - flags, - stream_data.id, - stream_data, - stream_data.count, - ts); -} - // ============================================================================ // The DefaultApplication is the default implementation of Session::Application // that is used for all unrecognized ALPN identifiers. @@ -760,6 +318,12 @@ class DefaultApplication final : public Session::Application { // during the onstream callback (via MakeCallback re-entrancy). return false; } + + // The stream was created, but was immediately destroyed because there's + // no onstream handler. + if (stream->is_destroyed()) [[unlikely]] { + return true; + } } else { stream = BaseObjectPtr(Stream::From(stream_user_data)); if (!stream) { @@ -777,11 +341,11 @@ class DefaultApplication final : public Session::Application { return true; } - int GetStreamData(StreamData* stream_data) override { + int GetStreamData(Session::StreamData* stream_data) override { // Reset the state of stream_data before proceeding... stream_data->id = -1; stream_data->count = 0; - stream_data->fin = 0; + stream_data->fin = false; stream_data->stream.reset(); Debug(&session(), "Default application getting stream data"); DCHECK_NOT_NULL(stream_data); @@ -804,7 +368,7 @@ class DefaultApplication final : public Session::Application { case bob::Status::STATUS_WAIT: return; case bob::Status::STATUS_EOS: - stream_data->fin = 1; + stream_data->fin = true; } // It is possible that the data pointers returned are not actually @@ -835,10 +399,10 @@ class DefaultApplication final : public Session::Application { arraysize(stream_data->data), kMaxVectorCount); if (ret == bob::Status::STATUS_EOS) { - stream_data->fin = 1; + stream_data->fin = true; } } else { - stream_data->fin = 1; + stream_data->fin = true; } return 0; @@ -864,7 +428,7 @@ class DefaultApplication final : public Session::Application { stream->Schedule(&stream_queue_); } - bool StreamCommit(StreamData* stream_data, size_t datalen) override { + bool StreamCommit(Session::StreamData* stream_data, size_t datalen) override { DCHECK_NOT_NULL(stream_data); CHECK(stream_data->stream); stream_data->stream->Commit(datalen, stream_data->fin); diff --git a/src/quic/application.h b/src/quic/application.h index 0df9b9f0a0e68d..619b41dd8d0b12 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -205,10 +205,6 @@ class Session::Application : public MemoryRetainer { return false; } - // Signals to the Application that it should serialize and transmit any - // pending session and stream packets it has accumulated. - void SendPendingData(); - // Returns true if the application protocol supports sending and // receiving headers on streams (e.g. HTTP/3). Applications that // do not support headers should return false (the default). @@ -243,10 +239,6 @@ class Session::Application : public MemoryRetainer { return {StreamPriority::DEFAULT, StreamPriorityFlags::NON_INCREMENTAL}; } - // The StreamData struct is used by the application to pass pending stream - // data to the session for transmission. - struct StreamData; - virtual int GetStreamData(StreamData* data) = 0; virtual bool StreamCommit(StreamData* data, size_t datalen) = 0; @@ -262,57 +254,9 @@ class Session::Application : public MemoryRetainer { } private: - Packet::Ptr CreateStreamDataPacket(); - - // Tries to pack a pending datagram into the current packet buffer. - // If < 0 is returned, either NGTCP2_ERR_WRITE_MORE or a fatal error is - // returned; the caller must check. If > 0 is returned, the packet is done - // and the value is the size of the finalized packet. If 0 is returned, - // the datagram is either congestion limited or was abandoned - ssize_t TryWritePendingDatagram(PathStorage* path, - uint8_t* dest, - size_t destlen, - uint64_t ts); - - // Write the given stream_data into the buffer. The PacketInfo out-param - // is populated by ngtcp2 with per-packet metadata (e.g., ECN codepoint) - // that should be applied when sending the packet. - ssize_t WriteVStream(PathStorage* path, - PacketInfo* pi, - uint8_t* buf, - ssize_t* ndatalen, - size_t max_packet_size, - const StreamData& stream_data, - uint64_t ts); - Session* session_ = nullptr; }; -struct Session::Application::StreamData final { - // The actual number of vectors in the struct, up to kMaxVectorCount. - size_t count = 0; - // The stream identifier. If this is a negative value then no stream is - // identified. - stream_id id = -1; - int fin = 0; - ngtcp2_vec data[kMaxVectorCount]{}; - BaseObjectPtr stream; - - static_assert(sizeof(ngtcp2_vec) == sizeof(nghttp3_vec) && - alignof(ngtcp2_vec) == alignof(nghttp3_vec) && - offsetof(ngtcp2_vec, base) == offsetof(nghttp3_vec, base) && - offsetof(ngtcp2_vec, len) == offsetof(nghttp3_vec, len), - "ngtcp2_vec and nghttp3_vec must have identical layout"); - inline operator nghttp3_vec*() { - return reinterpret_cast(data); - } - - inline operator const ngtcp2_vec*() const { return data; } - inline operator ngtcp2_vec*() { return data; } - - std::string ToString() const; -}; - // Create a DefaultApplication for the given session. std::unique_ptr CreateDefaultApplication( Session* session, const Session::Application_Options& options); diff --git a/src/quic/http3.cc b/src/quic/http3.cc index bc479f96990577..efaf1959427058 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -209,15 +209,15 @@ class Http3ApplicationImpl final : public Session::Application { started_ = true; Debug(&session(), "Starting HTTP/3 application."); - auto params = ngtcp2_conn_get_remote_transport_params(session()); - if (params == nullptr) [[unlikely]] { + const auto params = session().remote_transport_params(); + if (!params) [[unlikely]] { // The params are not available yet. Cannot start. Debug(&session(), "Cannot start HTTP/3 application yet. No remote transport params"); return false; } - if (params->initial_max_streams_uni < 3) { + if (params.initial_max_streams_uni() < 3) { // HTTP3 requires 3 unidirectional control streams to be opened in each // direction in additional to the bidirectional streams that are used to // actually carry request and response payload back and forth. @@ -225,8 +225,9 @@ class Http3ApplicationImpl final : public Session::Application { // https://nghttp2.org/nghttp3/programmers-guide.html#binding-control-streams Debug(&session(), "Cannot start HTTP/3 application. Initial max " - "unidirectional streams [%zu] is too low. Must be at least 3", - params->initial_max_streams_uni); + "unidirectional streams [%" PRIu64 + "] is too low. Must be at least 3", + params.initial_max_streams_uni()); return false; } @@ -235,17 +236,14 @@ class Http3ApplicationImpl final : public Session::Application { // of requests that the client can actually created. if (session().is_server()) { nghttp3_conn_set_max_client_streams_bidi( - *this, params->initial_max_streams_bidi); + *this, params.initial_max_streams_bidi()); } Debug(&session(), "Creating and binding HTTP/3 control streams"); bool ret = - ngtcp2_conn_open_uni_stream(session(), &control_stream_id_, nullptr) == - 0 && - ngtcp2_conn_open_uni_stream( - session(), &qpack_enc_stream_id_, nullptr) == 0 && - ngtcp2_conn_open_uni_stream( - session(), &qpack_dec_stream_id_, nullptr) == 0 && + session().OpenUnidirectionalStream(&control_stream_id_) && + session().OpenUnidirectionalStream(&qpack_enc_stream_id_) && + session().OpenUnidirectionalStream(&qpack_dec_stream_id_) && nghttp3_conn_bind_control_stream(*this, control_stream_id_) == 0 && nghttp3_conn_bind_qpack_streams( *this, qpack_enc_stream_id_, qpack_dec_stream_id_) == 0; @@ -306,8 +304,7 @@ class Http3ApplicationImpl final : public Session::Application { Debug(&session(), "Extending stream and connection offset by %zd bytes", nread); - session().ExtendStreamOffset(id, nread); - session().ExtendOffset(nread); + session().Consume(id, nread); } // If this data arrived as 0-RTT, mark the stream. We set it after @@ -365,24 +362,11 @@ class Http3ApplicationImpl final : public Session::Application { case EndpointLabel::LOCAL: return; case EndpointLabel::REMOTE: { - switch (direction) { - case Direction::BIDIRECTIONAL: { - Debug(&session(), - "HTTP/3 application extending max bidi streams by %" PRIu64, - max_streams); - ngtcp2_conn_extend_max_streams_bidi( - session(), static_cast(max_streams)); - break; - } - case Direction::UNIDIRECTIONAL: { - Debug(&session(), - "HTTP/3 application extending max uni streams by %" PRIu64, - max_streams); - ngtcp2_conn_extend_max_streams_uni( - session(), static_cast(max_streams)); - break; - } - } + Debug(&session(), + "HTTP/3 application extending max %s streams by %" PRIu64, + direction == Direction::BIDIRECTIONAL ? "bidi" : "uni", + max_streams); + session().ExtendMaxStreams(direction, max_streams); } } } @@ -530,8 +514,7 @@ class Http3ApplicationImpl final : public Session::Application { return; } - session().SetLastError( - QuicError::ForApplication(nghttp3_err_infer_quic_app_error_code(rv))); + session().SetApplicationError(nghttp3_err_infer_quic_app_error_code(rv)); session().Close(); } @@ -548,8 +531,7 @@ class Http3ApplicationImpl final : public Session::Application { return; } - session().SetLastError( - QuicError::ForApplication(nghttp3_err_infer_quic_app_error_code(rv))); + session().SetApplicationError(nghttp3_err_infer_quic_app_error_code(rv)); session().Close(); } @@ -687,17 +669,30 @@ class Http3ApplicationImpl final : public Session::Application { return {StreamPriority::DEFAULT, StreamPriorityFlags::NON_INCREMENTAL}; } - int GetStreamData(StreamData* data) override { + int GetStreamData(Session::StreamData* data) override { + static_assert( + sizeof(ngtcp2_vec) == sizeof(nghttp3_vec) && + alignof(ngtcp2_vec) == alignof(nghttp3_vec) && + offsetof(ngtcp2_vec, base) == offsetof(nghttp3_vec, base) && + offsetof(ngtcp2_vec, len) == offsetof(nghttp3_vec, len), + "ngtcp2_vec and nghttp3_vec must have identical layout"); data->count = kMaxVectorCount; ssize_t ret = 0; Debug(&session(), "HTTP/3 application getting stream data"); if (conn_ && session().max_data_left()) { - ret = nghttp3_conn_writev_stream( - *this, &data->id, &data->fin, *data, data->count); + // nghttp3 reports fin through an int out-param; bridge it to the bool. + int fin = 0; + ret = + nghttp3_conn_writev_stream(*this, + &data->id, + &fin, + reinterpret_cast(data->data), + data->count); // A negative return value indicates an error. if (ret < 0) { return static_cast(ret); } + data->fin = fin != 0; data->count = static_cast(ret); if (data->id >= 0 && data->id != control_stream_id_ && @@ -710,7 +705,7 @@ class Http3ApplicationImpl final : public Session::Application { return 0; } - bool StreamCommit(StreamData* data, size_t datalen) override { + bool StreamCommit(Session::StreamData* data, size_t datalen) override { Debug(&session(), "HTTP/3 application committing stream %" PRIi64 " data %zu", data->id, @@ -720,8 +715,7 @@ class Http3ApplicationImpl final : public Session::Application { // nghttp3 tracks its own offset via add_write_offset. int err = nghttp3_conn_add_write_offset(*this, data->id, datalen); if (err != 0) { - session().SetLastError(QuicError::ForApplication( - nghttp3_err_infer_quic_app_error_code(err))); + session().SetApplicationError(nghttp3_err_infer_quic_app_error_code(err)); return false; } // Raw application bytes are committed to the stream's outbound @@ -921,24 +915,25 @@ class Http3ApplicationImpl final : public Session::Application { stream->ReceiveData(nullptr, 0, flags); } - void OnStopSending(stream_id id, error_code app_error_code) { + void OnSendStopSending(stream_id id, error_code app_error_code) { auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), - "HTTP/3 application received stop sending for stream %" PRIi64, + "HTTP/3 application should send stop sending for stream %" PRIi64, id); - stream->ReceiveStopSending(QuicError::ForApplication(app_error_code)); + stream->SendStopSending(app_error_code); } - void OnResetStream(stream_id id, error_code app_error_code) { + void OnDoResetStream(stream_id id, error_code app_error_code) { auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), - "HTTP/3 application received reset stream for stream %" PRIi64, + "HTTP/3 application received a request to reset stream for stream " + "%" PRIi64, id); - stream->ReceiveStreamReset(0, QuicError::ForApplication(app_error_code)); + stream->DoStreamReset(app_error_code); } void OnShutdown(stream_id id) { @@ -1211,10 +1206,10 @@ class Http3ApplicationImpl final : public Session::Application { void* conn_user_data, void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); - auto& session = app.session(); - Debug(&session, "HTTP/3 application deferred consume %zu bytes", consumed); - session.ExtendStreamOffset(id, consumed); - session.ExtendOffset(consumed); + Debug(&app.session(), + "HTTP/3 application deferred consume %zu bytes", + consumed); + app.session().Consume(id, consumed); return NGTCP2_SUCCESS; } @@ -1318,29 +1313,31 @@ class Http3ApplicationImpl final : public Session::Application { return NGTCP2_SUCCESS; } - static int on_stop_sending(nghttp3_conn* conn, - stream_id id, - error_code app_error_code, - void* conn_user_data, - void* stream_user_data) { + static int on_send_stop_sending(nghttp3_conn* conn, + stream_id id, + error_code app_error_code, + void* conn_user_data, + void* stream_user_data) { + // this callback asks the app side to send a stop sending NGHTTP3_CALLBACK_SCOPE(app); if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnStopSending(id, app_error_code); + app.OnSendStopSending(id, app_error_code); return NGTCP2_SUCCESS; } - static int on_reset_stream(nghttp3_conn* conn, - stream_id id, - error_code app_error_code, - void* conn_user_data, - void* stream_user_data) { + static int on_do_reset_stream(nghttp3_conn* conn, + stream_id id, + error_code app_error_code, + void* conn_user_data, + void* stream_user_data) { + // this callback ask the app side to do a reset stream NGHTTP3_CALLBACK_SCOPE(app); if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnResetStream(id, app_error_code); + app.OnDoResetStream(id, app_error_code); return NGTCP2_SUCCESS; } @@ -1394,9 +1391,9 @@ class Http3ApplicationImpl final : public Session::Application { on_begin_trailers, on_receive_trailer, on_end_trailers, - on_stop_sending, + on_send_stop_sending, on_end_stream, - on_reset_stream, + on_do_reset_stream, on_shutdown, nullptr, // recv_settings (deprecated) on_receive_origin, diff --git a/src/quic/session.cc b/src/quic/session.cc index 8380e477c01e80..4c162b821911b4 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -860,11 +860,11 @@ struct Session::Impl final : public MemoryRetainer { } } - endpoint->RemoveSession(config_.scid, remote_address_); - auto& binding = BindingData::Get(env()); if (stats_slot_) GetSessionStatsArena(binding).ReleaseSlot(stats_slot_); if (state_slot_) GetSessionStateArena(binding).ReleaseSlot(state_slot_); + + endpoint->RemoveSession(config_.scid, remote_address_); } void MemoryInfo(MemoryTracker* tracker) const override { @@ -1730,10 +1730,461 @@ Session::SendPendingDataScope::~SendPendingDataScope() { Debug(session, "Send Scope Depth %zu", session->impl_->send_scope_depth_); if (--session->impl_->send_scope_depth_ == 0 && session->impl_->application_ && !session->impl_->handshake_deferred_) { - session->application().SendPendingData(); + session->SendPendingData(); + } +} + +// ============================================================================ + +std::string Session::StreamData::ToString() const { + DebugIndentScope indent; + + size_t total_bytes = 0; + for (size_t n = 0; n < count; n++) { + total_bytes += data[n].len; + } + + auto prefix = indent.Prefix(); + std::string res("{"); + res += prefix + "count: " + std::to_string(count); + res += prefix + "id: " + std::to_string(id); + res += prefix + "fin: " + std::to_string(fin); + res += prefix + "total: " + std::to_string(total_bytes); + res += indent.Close(); + return res; +} + +Packet::Ptr Session::CreateStreamDataPacket() { + return endpoint().CreatePacket( + remote_address(), max_packet_size(), "stream data"); +} + +// Attempts to pack a pending datagram into the current packet. +// Returns the nwrite value from ngtcp2_conn_writev_datagram. +// On fatal error, closes the session and returns the error code. +// The caller should check: +// > 0: packet is complete, send it (pos was NOT advanced — caller +// must add nwrite to pos and send) +// NGTCP2_ERR_WRITE_MORE: datagram packed, room for more +// 0: congestion controlled or doesn't fit, datagram stays in queue +// < 0 (other): fatal error, session already closed +ssize_t Session::TryWritePendingDatagram(PathStorage* path, + uint8_t* dest, + size_t destlen, + uint64_t ts) { + CHECK(HasPendingDatagrams()); + auto max_attempts = config().options.max_datagram_send_attempts; + + // Skip datagrams that have already exceeded the send attempt limit + // from a previous SendPendingData cycle. + while (HasPendingDatagrams()) { + auto& front = PeekPendingDatagram(); + if (front.send_attempts < max_attempts) break; + Debug(this, + "Datagram %" PRIu64 " abandoned after %u attempts", + front.id, + front.send_attempts); + DatagramStatus(front.id, DatagramStatus::ABANDONED); + PopPendingDatagram(); + } + + if (!HasPendingDatagrams()) return 0; + auto& dg = PeekPendingDatagram(); + ngtcp2_vec dgvec = dg.data; + int accepted = 0; + int dg_flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; + + // PacketInfo for the datagram path. When libuv gains per-socket ECN + // marking, the value from ngtcp2 should be forwarded to the send path. + PacketInfo dg_pi; + ssize_t dg_nwrite = ngtcp2_conn_writev_datagram(*this, + &path->path, + dg_pi, + dest, + destlen, + &accepted, + dg_flags, + dg.id, + &dgvec, + 1, + ts); + + if (accepted) { + // Nice, the datagram was accepted! + Debug(this, "Datagram %" PRIu64 " accepted into packet", dg.id); + DatagramSent(dg.id); + PopPendingDatagram(); + } else { + Debug(this, "Datagram %" PRIu64 " not accepted into packet", dg.id); + } + + // ngtcp2 can return a positive number if the packet was nearly full, and so + // it finalized without waiting for more. This isn't an error, and can happen + // with or without `accepted` being true (if false, it stays queued). + if (dg_nwrite > 0) { + if (!accepted) dg.send_attempts++; + return dg_nwrite; + } + + switch (dg_nwrite) { + case 0: { + // If dg_nwrite is 0, we are either congestion controlled or + // there wasn't enough room in the packet for the datagram or + // we aren't in a state where we can send. + // We'll skip this attempt and return 0. + CHECK(!accepted); + dg.send_attempts++; + return 0; + } + case NGTCP2_ERR_WRITE_MORE: { + // There's still room left in the packet! + return NGTCP2_ERR_WRITE_MORE; + } + case NGTCP2_ERR_INVALID_STATE: + case NGTCP2_ERR_INVALID_ARGUMENT: { + // Non-fatal error cases. Peer either does not support datagrams + // or the datagram is too large for the peer's max. + // Abandon the datagram and signal skip by returning std::nullopt. + DatagramStatus(dg.id, DatagramStatus::ABANDONED); + PopPendingDatagram(); + return 0; + } + default: { + // Fatal errors: PKT_NUM_EXHAUSTED, CALLBACK_FAILURE, NOMEM, etc. + Debug(this, "Fatal datagram error: %zd", dg_nwrite); + SetLastError(QuicError::ForNgtcp2Error(dg_nwrite)); + Close(CloseMethod::SILENT); + return dg_nwrite; + } + } + UNREACHABLE(); +} + +// the SendPendingData method is the primary driver for sending data from the +// application layer. It loops through available stream data and pending +// datagrams and generates packets to send until there is either no more +// data to send or we hit the maximum number of packets to send in one go. +// This method is extremely delicate. A bug in this method can break the +// entire QUIC implementation; so be very careful when making changes here +// and make sure to test thoroughly. When in doubt... don't change it. +void Session::SendPendingData() { + DCHECK(!is_destroyed()); + if (!can_send_packets()) [[unlikely]] { + return; + } + // Upper bound on packets per SendPendingData call. ngtcp2's send quantum + // is typically 64 KB, which at 1200-byte minimum packet size is ~53 + // packets. 64 covers the worst case with headroom. The actual count per + // call is dynamically capped by ngtcp2_conn_get_send_quantum(). + static constexpr size_t kMaxPackets = 64; + Debug(this, "Session sending pending data"); + // Cache the timestamp once for the entire send loop. ngtcp2 does not + // require nanosecond-accurate monotonicity within a single burst — + // a single timestamp per SendPendingData call is what other QUIC + // implementations use (e.g., quiche, msquic). When kernel-level + // packet pacing becomes available via libuv, this timestamp becomes + // the base for computing per-packet transmit timestamps. + const uint64_t ts = uv_hrtime(); + PathStorage path; + StreamData stream_data; + + bool closed = false; + + // Batch accumulation: packets are collected here and flushed via + // Session::SendBatch when the loop exits, the batch is full, or + // on early return. This enables synchronous batched delivery via + // uv_udp_try_send2 (sendmmsg) from the deferred flush path. + Packet::Ptr batch[kMaxPackets]; + PathStorage batch_paths[kMaxPackets]; + size_t batch_count = 0; + + auto flush_batch = [&] { + if (batch_count == 0) return; + SendBatch(batch, batch_paths, batch_count); + batch_count = 0; + }; + + auto update_stats = OnScopeLeave([&] { + if (closed) return; + // Flush any remaining accumulated packets before updating stats. + flush_batch(); + if (is_destroyed()) [[unlikely]] + return; + + // Get a strong pointer to protect against potential destruction during + // updating the time and data stats. + BaseObjectPtr s(this); + s->UpdatePacketTxTime(); + s->UpdateTimer(); + s->UpdateDataStats(); + }); + + // The maximum number of packets to send in this call to SendPendingData. + const size_t max_packet_count = std::min( + kMaxPackets, ngtcp2_conn_get_send_quantum(*this) / max_packet_size()); + if (max_packet_count == 0) return; + + // The number of packets that have been prepared in this call. + size_t packet_send_count = 0; + + Packet::Ptr packet; + + auto ensure_packet = [&] { + if (!packet) { + packet = CreateStreamDataPacket(); + if (!packet) [[unlikely]] + return false; + } + DCHECK(packet); + return true; + }; + + // Accumulate a completed packet into the batch. + auto enqueue_packet = + [&](Packet::Ptr& pkt, size_t len, const PacketInfo& pi) { + Debug(this, "Enqueuing packet with %zu bytes into batch", len); + pkt->Truncate(len); + pkt->set_pkt_info(pi); + path.CopyTo(&batch_paths[batch_count]); + batch[batch_count++] = std::move(pkt); + }; + + // We're going to enter a loop here to prepare and send no more than + // max_packet_count packets. + for (;;) { + // ndatalen is the amount of stream data that was accepted into the packet. + ssize_t ndatalen = 0; + + // Make sure we have a packet to write data into. + if (!ensure_packet()) [[unlikely]] { + Debug(this, "Failed to create packet for stream data"); + // Doh! Could not create a packet. Time to bail. + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + closed = true; + return Close(CloseMethod::SILENT); + } + + // The stream_data is the next block of data from the application stream. + if (application().GetStreamData(&stream_data) < 0) { + Debug(this, "Application failed to get stream data"); + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + closed = true; + return Close(CloseMethod::SILENT); + } + + // If we got here, we were at least successful in checking for stream data. + // There might not be any stream data to send. If there is no stream data, + // that's perfectly fine, we still need to serialize any frames we do have + // (pings, acks, datagrams, etc) so we'll just keep going. + if (stream_data.id >= 0) { + Debug(this, "Session using stream data: %s", stream_data); + } else { + Debug(this, "No stream data to send"); + } + if (HasPendingDatagrams()) { + Debug(this, "There are pending datagrams to send"); + } + + // Awesome, let's write our packet! + PacketInfo pi; + ssize_t nwrite = WriteVStream(&path, + &pi, + packet->data(), + &ndatalen, + packet->length(), + stream_data, + ts); + + // When ndatalen is > 0, that's our indication that stream data was + // accepted in to the packet. Yay! + if (ndatalen > 0) { + Debug(this, + "Session accepted %zu bytes from stream %" PRIi64 " into packet", + ndatalen, + stream_data.id); + if (!application().StreamCommit(&stream_data, ndatalen)) { + // Data was accepted into the packet, but for some reason adjusting + // the stream's committed data failed. Treat as fatal. + Debug(this, "Failed to commit accepted bytes in stream"); + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + closed = true; + return Close(CloseMethod::SILENT); + } + // StreamCommit can call into JS, so a user callback could + // have destroyed the session: + if (is_destroyed()) [[unlikely]] { + closed = true; + return; + } + } else if (stream_data.id >= 0) { + Debug(this, + "Session did not accept any bytes from stream %" PRIi64 + " into packet", + stream_data.id); + } + + // When nwrite is zero, it means we are congestion limited or it is + // just not our turn to send something. Re-schedule the stream if it + // had unsent data (payload or FIN) so the next timer-triggered + // SendPendingData retries it. Without this, a FIN-only send that + // hits nwrite=0 is lost forever — the stream already returned EOS + // from Pull and won't be re-scheduled by anyone else. + // We call Application::ResumeStream directly (not Session::ResumeStream) + // to avoid creating a SendPendingDataScope — we're already inside + // SendPendingData and re-entering would just hit nwrite=0 again. + if (nwrite == 0 && (stream_data.id >= 0 || !HasPendingDatagrams())) { + Debug(this, "Congestion or not our turn to send"); + if (stream_data.id >= 0 && (stream_data.count > 0 || stream_data.fin)) { + application().ResumeStream(stream_data.id); + } + return; + } + + // A negative nwrite value indicates either an error or that there is more + // data to write into the packet. + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: { + // We could not write any data for this stream into the packet + // because the flow control for the stream itself indicates that + // the stream is blocked. We'll skip and move on to the next stream. + // ndatalen = -1 means that no stream data was accepted into the + // packet, which is what we want here. + DCHECK_EQ(ndatalen, -1); + // We should only have received this error if there was an actual + // stream identified in the stream data, but let's double check. + DCHECK_GE(stream_data.id, 0); + StreamDataBlocked(stream_data.id); + continue; + } + case NGTCP2_ERR_STREAM_SHUT_WR: { + // Indicates that the writable side of the stream should be closed + // locally or the stream is being reset. In either case, we can't + // send data for this stream! + Debug(this, "Closing stream %" PRIi64 " for writing", stream_data.id); + // ndatalen = -1 means that no stream data was accepted into the + // packet, which is what we want here. + DCHECK_EQ(ndatalen, -1); + // We should only have received this error if there was an actual + // stream identified in the stream data, but let's double check. + DCHECK_GE(stream_data.id, 0); + if (stream_data.stream) [[likely]] { + stream_data.stream->EndWritable(); + } + // Notify the application that the stream's write side is shut + // so it stops queuing data. Without this, GetStreamData would + // keep returning the same stream and we'd loop forever. + application().StreamWriteShut(stream_data.id); + continue; + } + case NGTCP2_ERR_WRITE_MORE: { + Debug(this, "Packet buffer not full, coalesce more data into it"); + // Room for more in this packet. Try to pack a pending datagram + // if there is one. Otherwise just loop around and keep going. + if (HasPendingDatagrams()) { + auto result = TryWritePendingDatagram( + &path, packet->data(), packet->length(), ts); + // When result is 0, either the datagram was congestion controlled, + // didn't fit in the packet, or was abandoned. Skip and continue. + + // When result is > 0, the packet is done and the result is the + // completed size of the packet we're sending. + if (result > 0) { + size_t len = result; + Debug(this, "Sending packet with %zu bytes", len); + enqueue_packet(packet, len, pi); + if (++packet_send_count == max_packet_count) return; + } else if (result < 0) { + // Any negative result other than NGTCP2_ERR_WRITE_MORE + // at this point is fatal. The session will have been + // closed. + if (result != NGTCP2_ERR_WRITE_MORE) return; + } + } + continue; + } + case NGTCP2_ERR_CALLBACK_FAILURE: { + // This case really should not happen. It indicates that the + // ngtcp2 callback failed for some reason. This would be a + // bug in our code. + Debug(this, "Internal failure with ngtcp2 callback"); + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + closed = true; + return Close(CloseMethod::SILENT); + } + } + + // Some other type of error happened. + DCHECK_EQ(ndatalen, -1); + Debug(this, + "Session encountered error while writing packet: %s", + ngtcp2_strerror(nwrite)); + SetLastError(QuicError::ForNgtcp2Error(nwrite)); + closed = true; + return Close(CloseMethod::SILENT); + } + + // At this point we have a packet prepared to send. The nwrite + // is the size of the packet we are sending. + size_t len = nwrite; + Debug(this, "Sending packet with %zu bytes", len); + enqueue_packet(packet, len, pi); + if (++packet_send_count == max_packet_count) return; + + // If there are pending datagrams, try sending them in a fresh packet. + // This is necessary because ngtcp2_conn_writev_stream only returns + // NGTCP2_ERR_WRITE_MORE when there is actual stream data — when no + // streams are active, the coalescing path above is never reached and + // datagrams would never be sent. + if (HasPendingDatagrams()) { + if (!ensure_packet()) [[unlikely]] { + Debug(this, "Failed to create packet for datagram"); + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + closed = true; + return Close(CloseMethod::SILENT); + } + auto result = + TryWritePendingDatagram(&path, packet->data(), packet->length(), ts); + if (result > 0) { + Debug(this, "Sending datagram packet with %zd bytes", result); + enqueue_packet(packet, static_cast(result), PacketInfo()); + if (++packet_send_count == max_packet_count) return; + } else if (result < 0 && result != NGTCP2_ERR_WRITE_MORE) { + // Fatal error — session already closed by TryWritePendingDatagram. + return; + } + // If result == 0 (congestion) or NGTCP2_ERR_WRITE_MORE (datagram + // packed but room for more), the loop continues normally. + } } } +ssize_t Session::WriteVStream(PathStorage* path, + PacketInfo* pi, + uint8_t* dest, + ssize_t* ndatalen, + size_t max_packet_size, + const StreamData& stream_data, + uint64_t ts) { + DCHECK_LE(stream_data.count, kMaxVectorCount); + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + // The PacketInfo out-param is populated by ngtcp2 with the ECN codepoint + // to apply when sending this packet. When libuv gains per-socket ECN + // marking, the value should be forwarded to the send path. + return ngtcp2_conn_writev_stream(*this, + &path->path, + *pi, + dest, + max_packet_size, + ndatalen, + flags, + stream_data.id, + stream_data, + stream_data.count, + ts); +} + // ============================================================================ BaseObjectPtr Session::Create( Endpoint* endpoint, @@ -2469,7 +2920,7 @@ void Session::FlushPendingData() { // Prefer synchronous sends during the deferred flush to avoid the // one-tick latency of async uv_udp_send from the uv_check callback. flags_.prefer_try_send = true; - application().SendPendingData(); + SendPendingData(); flags_.prefer_try_send = false; } } @@ -2593,6 +3044,7 @@ datagram_id Session::SendDatagram(Store&& data) { return did; } } + Session::SendPendingDataScope send_scope(this); // Queue the datagram. It will be serialized into packets by // SendPendingData alongside stream data. @@ -2797,7 +3249,8 @@ void Session::RemoveStream(stream_id id) { // then we can proceed to finishing the close now. Note that the // expectation is that the session will be destroyed once FinishClose // returns. - if (impl_->state()->closing && impl_->state()->graceful_close) { + if (impl_->state()->closing && impl_->state()->graceful_close && + impl_->streams_.size() == 0) { FinishClose(); CHECK(is_destroyed()); } @@ -2959,6 +3412,30 @@ uint64_t Session::max_data_left() const { return ngtcp2_conn_get_max_data_left(*this); } +bool Session::OpenUnidirectionalStream(stream_id* id) { + return ngtcp2_conn_open_uni_stream(*this, id, nullptr) == 0; +} + +void Session::ExtendMaxStreams(Direction direction, uint64_t max) { + switch (direction) { + case Direction::BIDIRECTIONAL: + ngtcp2_conn_extend_max_streams_bidi(*this, static_cast(max)); + break; + case Direction::UNIDIRECTIONAL: + ngtcp2_conn_extend_max_streams_uni(*this, static_cast(max)); + break; + } +} + +void Session::Consume(stream_id id, size_t len) { + ExtendStreamOffset(id, len); + ExtendOffset(len); +} + +void Session::SetApplicationError(error_code app_error_code) { + SetLastError(QuicError::ForApplication(app_error_code)); +} + uint64_t Session::max_local_streams_uni() const { DCHECK(!is_destroyed()); return ngtcp2_conn_get_streams_uni_left(*this); @@ -3157,7 +3634,7 @@ void Session::OnTimeout() { // which can synchronously destroy the session. Guard before proceeding. if (is_destroyed()) return; if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { - application().SendPendingData(); + SendPendingData(); if (is_destroyed()) return; CheckStreamIdleTimeout(uv_hrtime()); return; diff --git a/src/quic/session.h b/src/quic/session.h index 0caeb764ba56c8..070f98dc938d7b 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -104,6 +104,25 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // of a QUIC Session. class Application; + // A block of pending outbound stream data, passed between the application + // layer (which fills it via GetStreamData) and the send pump (which hands + // it to ngtcp2_conn_writev_stream and commits the accepted length). + struct StreamData final { + // The actual number of vectors in the struct, up to kMaxVectorCount. + size_t count = 0; + // The stream identifier. If this is a negative value then no stream is + // identified. + stream_id id = -1; + bool fin = false; + ngtcp2_vec data[kMaxVectorCount]{}; + BaseObjectPtr stream; + + inline operator const ngtcp2_vec*() const { return data; } + inline operator ngtcp2_vec*() { return data; } + + std::string ToString() const; + }; + // Decode the first ALPN protocol name from wire format (length-prefixed). static std::string_view DecodeAlpn(std::string_view wire); @@ -417,6 +436,36 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // bindingdata.cc doesn't need the full Application type definition. void FlushPendingData(); + // The send pump: the primary driver for serializing outbound data. + // Loops through available stream data (pulled from the application + // layer via GetStreamData/StreamCommit) and pending datagrams, + // generating packets until there is no more data to send or the + // packet budget for this call is exhausted. + void SendPendingData(); + + Packet::Ptr CreateStreamDataPacket(); + + // Tries to pack a pending datagram into the current packet buffer. + // If a negative value is returned, it is either NGTCP2_ERR_WRITE_MORE or + // fatal error; the caller must check. If > 0 is returned, the packet is done + // and the value is the size of the finalized packet. If 0 is returned, + // the datagram is either congestion limited or was abandoned. + ssize_t TryWritePendingDatagram(PathStorage* path, + uint8_t* dest, + size_t destlen, + uint64_t ts); + + // Write the given stream_data into the buffer. The PacketInfo out-param + // is populated by ngtcp2 with per-packet metadata (e.g., ECN codepoint) + // that should be applied when sending the packet. + ssize_t WriteVStream(PathStorage* path, + PacketInfo* pi, + uint8_t* buf, + ssize_t* ndatalen, + size_t max_packet_size, + const StreamData& stream_data, + uint64_t ts); + // Send a batch of packets accumulated by SendPendingData. Uses // Endpoint::SendBatch (uv_udp_try_send2 / sendmmsg) for synchronous // batched delivery when called from the deferred flush path. @@ -486,6 +535,21 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void SetLastError(QuicError&& error); uint64_t max_data_left() const; + // Transport operations that protocol applications (e.g. HTTP/3) invoke on + // the session, encapsulating ngtcp2 transport details here. + + // Open a unidirectional stream, setting *id on success, or returning false + bool OpenUnidirectionalStream(stream_id* id); + + void ExtendMaxStreams(Direction direction, uint64_t max); + + // Signal that we've consumed `len` bytes on stream `id` to update flow + // control + void Consume(stream_id id, size_t len); + + // Record an application-level error on the connection without closing it. + void SetApplicationError(error_code app_error_code); + PendingStream::PendingStreamQueue& pending_bidi_stream_queue() const; PendingStream::PendingStreamQueue& pending_uni_stream_queue() const; diff --git a/src/quic/streams.cc b/src/quic/streams.cc index e838392361f946..2670fc87d6bc21 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -487,15 +487,7 @@ struct Stream::Impl { code = args[0].As()->Uint64Value(&unused); } - stream->EndReadable(); - - if (!stream->is_pending()) { - // If the stream is a local unidirectional there's nothing to do here. - if (stream->is_local_unidirectional()) return; - stream->NotifyReadableEnded(code); - } else { - stream->pending_close_read_code_ = code; - } + stream->SendStopSending(code); } // Sends a reset stream to the peer to tell it we will not be sending any @@ -512,21 +504,7 @@ struct Stream::Impl { code = args[0].As()->Uint64Value(&lossless); } - if (stream->state()->reset == 1) return; - - stream->EndWritable(); - // We can release our outbound here now. Since the stream is being reset - // on the ngtcp2 side, we do not need to keep any of the data around - // waiting for acknowledgement that will never come. - stream->outbound_.reset(); - stream->state()->reset = 1; - - if (!stream->is_pending()) { - if (stream->is_remote_unidirectional()) return; - stream->NotifyWritableEnded(code); - } else { - stream->pending_close_write_code_ = code; - } + stream->DoStreamReset(code); } JS_METHOD(SetPriority) { @@ -1325,6 +1303,10 @@ bool Stream::is_pending() const { return state()->pending; } +bool Stream::is_destroyed() const { + return stats()->destroyed_at != 0; +} + stream_id Stream::id() const { return state()->id; } @@ -1520,8 +1502,7 @@ void Stream::EntryRead(size_t amount) { // Extend the flow control window so the sender can transmit more. if (session().is_destroyed()) return; Session::SendPendingDataScope send_scope(&session()); - session().ExtendStreamOffset(id(), amount); - session().ExtendOffset(amount); + session().Consume(id(), amount); } void Stream::BeforePull() { @@ -1823,6 +1804,36 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { EmitReset(error); } +void Stream::DoStreamReset(error_code code) { + if (state()->reset == 1) return; + + EndWritable(); + // We can release our outbound here now. Since the stream is being reset + // on the ngtcp2 side, we do not need to keep any of the data around + // waiting for acknowledgement that will never come. + outbound_.reset(); + state()->reset = 1; + + if (!is_pending()) { + if (is_remote_unidirectional()) return; + NotifyWritableEnded(code); + } else { + pending_close_write_code_ = code; + } +} + +void Stream::SendStopSending(error_code code) { + EndReadable(); + + if (!is_pending()) { + // If the stream is a local unidirectional there's nothing to do here. + if (is_local_unidirectional()) return; + NotifyReadableEnded(code); + } else { + pending_close_read_code_ = code; + } +} + // ============================================================================ void Stream::EmitBlocked() { diff --git a/src/quic/streams.h b/src/quic/streams.h index 86cb36b2668985..cef7bb66031f38 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -267,6 +267,9 @@ class Stream final : public AsyncWrap, // to be created. bool is_pending() const; + // True if the stream is already destroyed. + bool is_destroyed() const; + // True if we've completely sent all outbound data for this stream. // Importantly, this does not necessarily mean that we are completely // done with the outbound data. We may still be waiting on outbound @@ -341,6 +344,17 @@ class Stream final : public AsyncWrap, void ReceiveStopSending(QuicError error); void ReceiveStreamReset(uint64_t final_size, QuicError error); + // Sends a reset stream to the peer to tell it we will not be sending any + // more data for this stream. This has the effect of shutting down the + // writable side of the stream for this peer. Any data that is held in the + // outbound queue will be dropped. The stream may still be readable. + void DoStreamReset(error_code code); + + // Tells the peer to stop sending data for this stream. This has the effect + // of shutting down the readable side of the stream for this peer. Any data + // that has already been received is still readable. + void SendStopSending(error_code code); + // Currently, only HTTP/3 streams support headers. These methods are here // to support that. They are not used when using any other QUIC application. diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 183ba973ac1823..b49e527b038069 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -506,6 +506,16 @@ TransportParams::operator bool() const { return ptr_ != nullptr; } +uint64_t TransportParams::initial_max_streams_bidi() const { + DCHECK_NOT_NULL(ptr_); + return ptr_->initial_max_streams_bidi; +} + +uint64_t TransportParams::initial_max_streams_uni() const { + DCHECK_NOT_NULL(ptr_); + return ptr_->initial_max_streams_uni; +} + void TransportParams::Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STREAM_DATA); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_DATA); diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index 46724574611aff..bae20b9ac0d4f2 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -150,6 +150,9 @@ class TransportParams final { operator bool() const; + uint64_t initial_max_streams_bidi() const; + uint64_t initial_max_streams_uni() const; + // Returns a Store containing the encoded transport parameters. // If an error occurs during encoding, or if the parameters could // not be encoded, an empty Store will be returned. diff --git a/test/common/child_process.js b/test/common/child_process.js index d8c957bdf09ce4..c74154bb084f32 100644 --- a/test/common/child_process.js +++ b/test/common/child_process.js @@ -80,9 +80,9 @@ function expectSyncExit(caller, spawnArgs, { function logAndThrow() { const tag = `[process ${child.pid}]:`; console.error(`${tag} --- stderr ---`); - console.error(stderrStr === undefined ? child.stderr.toString() : stderrStr); + console.error(stderrStr === undefined ? (child.stderr?.toString() ?? '') : stderrStr); console.error(`${tag} --- stdout ---`); - console.error(stdoutStr === undefined ? child.stdout.toString() : stdoutStr); + console.error(stdoutStr === undefined ? (child.stdout?.toString() ?? '') : stdoutStr); console.error(`${tag} status = ${child.status}, signal = ${child.signal}`); const error = new Error(`${failures.join('\n')}`); diff --git a/test/es-module/test-defer-import-eval-with-tla.mjs b/test/es-module/test-defer-import-eval-with-tla.mjs new file mode 100644 index 00000000000000..48a1460f3acb38 --- /dev/null +++ b/test/es-module/test-defer-import-eval-with-tla.mjs @@ -0,0 +1,23 @@ +// Flags: --js-defer-import-eval + +// Tests that defer import of a module with top-level await +// eagerly evaluates the imported module. + +import '../common/index.mjs'; +import * as assert from 'assert'; + +// The importing module will only execute once its dependency containing +// top-level await has its promises resolved, as per +// https://github.com/tc39/proposal-top-level-await#what-exactly-is-blocked-by-a-top-level-await +globalThis.eval_list ||= []; + +import defer * as ns from '../fixtures/es-modules/dep-module-top-level-await.mjs'; + +// Check that the module has already been evaluated. +assert.strictEqual(globalThis.eval_list.length, 1); +assert.strictEqual(ns.foo, 42); +assert.strictEqual(globalThis.eval_list.length, 1); +assert.partialDeepStrictEqual(['defer-tla-1'], globalThis.eval_list); + +// Clean-up +delete globalThis.eval_list; diff --git a/test/es-module/test-defer-import-with-regular-import.mjs b/test/es-module/test-defer-import-with-regular-import.mjs new file mode 100644 index 00000000000000..6fd033446f819d --- /dev/null +++ b/test/es-module/test-defer-import-with-regular-import.mjs @@ -0,0 +1,24 @@ +// Flags: --js-defer-import-eval + +// Tests that defer import of a module that is also eagerly imported +// from another dependency gets executed synchronously, and gets +// executed exactly once. + +import '../common/index.mjs'; +import * as assert from 'assert'; + +globalThis.eval_list ||= []; + +import * as non_deferred from '../fixtures/es-modules/module-with-module-tree.mjs'; +import defer * as deferred from '../fixtures/es-modules/module-deferred-eval.mjs'; + +// Check that the module has been evaluated exactly once. +assert.strictEqual(globalThis.eval_list.length, 1); +assert.strictEqual(deferred.foo, 42); +assert.partialDeepStrictEqual(['defer-1'], globalThis.eval_list); + +// Skip linter warning +assert.strictEqual(non_deferred.bar, 64); + +// Clean-up +delete globalThis.eval_list; diff --git a/test/ffi/test-ffi-module.js b/test/ffi/test-ffi-module.js index 573c5e7ce5947b..ecbd6f1c6a9818 100644 --- a/test/ffi/test-ffi-module.js +++ b/test/ffi/test-ffi-module.js @@ -5,6 +5,7 @@ const { spawnSyncAndAssert } = require('../common/child_process'); const assert = require('node:assert'); const { spawnSync } = require('node:child_process'); const { test } = require('node:test'); +const { Worker } = require('node:worker_threads'); common.skipIfFFIMissing(); @@ -87,6 +88,7 @@ test('ffi exports expected API surface', () => { 'exportArrayBufferView', 'exportBuffer', 'exportString', + 'getCurrentEventLoop', 'getFloat32', 'getFloat64', 'getInt16', @@ -135,6 +137,7 @@ test('ffi exports expected API surface', () => { assert.strictEqual(typeof ffi.getUint64, 'function'); assert.strictEqual(typeof ffi.getFloat32, 'function'); assert.strictEqual(typeof ffi.getFloat64, 'function'); + assert.strictEqual(typeof ffi.getCurrentEventLoop, 'function'); assert.strictEqual(typeof ffi.setInt8, 'function'); assert.strictEqual(typeof ffi.setUint8, 'function'); assert.strictEqual(typeof ffi.setInt16, 'function'); @@ -180,3 +183,33 @@ test('ffi.types exports canonical type constants', () => { assert.deepStrictEqual(ffi.types, expected); assert.strictEqual(Object.isFrozen(ffi.types), true); }); + +test('ffi.getCurrentEventLoop returns the current thread event loop address', async () => { + const ffi = require('node:ffi'); + const mainLoop = ffi.getCurrentEventLoop(); + + assert.strictEqual(typeof mainLoop, 'bigint'); + assert.ok(mainLoop > 0n); + assert.strictEqual(ffi.getCurrentEventLoop(), mainLoop); + + const workerLoop = await new Promise((resolve, reject) => { + const worker = new Worker(` + const { parentPort } = require('node:worker_threads'); + const ffi = require('node:ffi'); + const loop = ffi.getCurrentEventLoop(); + + parentPort.postMessage([loop, ffi.getCurrentEventLoop()]); + `, { eval: true }); + + worker.on('message', resolve); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); + }); + }); + + assert.strictEqual(typeof workerLoop[0], 'bigint'); + assert.ok(workerLoop[0] > 0n); + assert.strictEqual(workerLoop[0], workerLoop[1]); + assert.notStrictEqual(workerLoop[0], mainLoop); +}); diff --git a/test/ffi/test-ffi-permissions.js b/test/ffi/test-ffi-permissions.js index c07f0bbdb439d7..970f5f019ee7b6 100644 --- a/test/ffi/test-ffi-permissions.js +++ b/test/ffi/test-ffi-permissions.js @@ -70,6 +70,10 @@ test('permission model blocks ffi memory and helper APIs', () => { ffi.getRawPointer(Buffer.alloc(0)); }, denied); + assert.throws(() => { + ffi.getCurrentEventLoop(); + }, denied); + assert.throws(() => { ffi.dlclose({ close() {} }); }, denied); diff --git a/test/fixtures/es-modules/dep-module-top-level-await.mjs b/test/fixtures/es-modules/dep-module-top-level-await.mjs new file mode 100644 index 00000000000000..e78c38f4db6e07 --- /dev/null +++ b/test/fixtures/es-modules/dep-module-top-level-await.mjs @@ -0,0 +1,10 @@ +import { setTimeout } from 'node:timers/promises'; + +if (!globalThis.eval_list) { + globalThis.eval_list = []; +} + +export const foo = 42; + +await setTimeout(1); +globalThis.eval_list.push('defer-tla-1'); diff --git a/test/fixtures/es-modules/module-deferred-eval.mjs b/test/fixtures/es-modules/module-deferred-eval.mjs index 181ac4c07f0600..afa9cbf1e52466 100644 --- a/test/fixtures/es-modules/module-deferred-eval.mjs +++ b/test/fixtures/es-modules/module-deferred-eval.mjs @@ -1,8 +1,6 @@ if (!globalThis.eval_list) { globalThis.eval_list = []; } -globalThis.eval_list.push('defer-1'); export const foo = 42; - -console.log('executed'); +globalThis.eval_list.push('defer-1'); diff --git a/test/fixtures/test-runner/entry-file/a.test.mjs b/test/fixtures/test-runner/entry-file/a.test.mjs new file mode 100644 index 00000000000000..3f41c33976751c --- /dev/null +++ b/test/fixtures/test-runner/entry-file/a.test.mjs @@ -0,0 +1,3 @@ +import { test } from 'node:test'; +import { runShared } from './helper.mjs'; +test('backup A', async (t) => { await runShared(t, 'A'); }); diff --git a/test/fixtures/test-runner/entry-file/b.test.mjs b/test/fixtures/test-runner/entry-file/b.test.mjs new file mode 100644 index 00000000000000..f555987816ebb5 --- /dev/null +++ b/test/fixtures/test-runner/entry-file/b.test.mjs @@ -0,0 +1,3 @@ +import { test } from 'node:test'; +import { runShared } from './helper.mjs'; +test('backup B', async (t) => { await runShared(t, 'B'); }); diff --git a/test/fixtures/test-runner/entry-file/helper.mjs b/test/fixtures/test-runner/entry-file/helper.mjs new file mode 100644 index 00000000000000..25fe93ca074598 --- /dev/null +++ b/test/fixtures/test-runner/entry-file/helper.mjs @@ -0,0 +1,3 @@ +export async function runShared(t, target) { + await t.test(`restore ${target}`, async () => {}); +} diff --git a/test/node-api/test_fatal/test2.js b/test/node-api/test_fatal/test2.js index 6449f098458d76..44866d63ee7d75 100644 --- a/test/node-api/test_fatal/test2.js +++ b/test/node-api/test_fatal/test2.js @@ -12,7 +12,7 @@ if (process.argv[2] === 'child') { } const p = child_process.spawnSync( - process.execPath, [ '--napi-modules', __filename, 'child' ]); + process.execPath, [ __filename, 'child' ]); assert.ifError(p.error); assert.ok(p.stderr.toString().includes( 'FATAL ERROR: test_fatal::Test fatal message')); diff --git a/test/parallel/test-crypto-dh-curves.js b/test/parallel/test-crypto-dh-curves.js index ddd5ea9377e63f..be4c2079d99884 100644 --- a/test/parallel/test-crypto-dh-curves.js +++ b/test/parallel/test-crypto-dh-curves.js @@ -5,6 +5,11 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); +const { + DH_CHECK_P_NOT_PRIME, + DH_CHECK_P_NOT_SAFE_PRIME, +} = crypto.constants; // Second OAKLEY group, see // https://github.com/nodejs/node-v0.x-archive/issues/2338 and @@ -15,12 +20,64 @@ const p = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' + 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF'; crypto.createDiffieHellman(p, 'hex'); +if (!process.features.openssl_is_boringssl) { + const notPrime = Buffer.from(p, 'hex'); + notPrime[notPrime.length - 1] = 0xfd; + assert.strictEqual( + crypto.createDiffieHellman(notPrime, Buffer.from([2])).verifyError, + DH_CHECK_P_NOT_PRIME); + + const notSafePrime = Buffer.from( + 'd2d6d13e1c1e0bbb63c742199dee010411f089ac74f0f7213348388280700fd6' + + '0ef9c1e7b096a4257dcbce61c544a5d1d23db4c49c63ce302f63be5cf5804327', + 'hex'); + assert.strictEqual( + crypto.createDiffieHellman(notSafePrime, Buffer.from([2])).verifyError, + DH_CHECK_P_NOT_SAFE_PRIME); + + const group = crypto.getDiffieHellman('modp14'); + const alice = crypto.createDiffieHellman( + group.getPrime(), group.getGenerator()); + alice.generateKeys(); + const groupPrime = BigInt(`0x${group.getPrime('hex')}`); + assert.throws( + () => alice.computeSecret(Buffer.from([1])), + { + code: 'ERR_CRYPTO_INVALID_KEYLEN', + message: 'Supplied key is too small' + }); + assert.throws( + () => alice.computeSecret(group.getPrime()), + { + code: 'ERR_CRYPTO_INVALID_KEYLEN', + message: 'Supplied key is too large' + }); + assert.throws( + () => alice.computeSecret( + Buffer.from((groupPrime - 1n).toString(16), 'hex')), + { + code: 'ERR_CRYPTO_INVALID_KEYLEN', + message: 'Supplied key is too large' + }); +} + // Confirm DH_check() results are exposed for optional examination. const bad_dh = process.features.openssl_is_boringssl ? crypto.createDiffieHellman('abcd', 'hex', 0) : crypto.createDiffieHellman('02', 'hex'); assert.notStrictEqual(bad_dh.verifyError, 0); +if (hasOpenSSL(3)) { + const smallSafePrime = crypto.createDiffieHellman( + Buffer.from([23]), Buffer.from([2])); + assert.notStrictEqual(smallSafePrime.verifyError, 0); + + assert.throws( + () => crypto.createDiffieHellman(Buffer.from(p, 'hex'), + Buffer.from(p, 'hex')), + { code: 'ERR_OSSL_DH_BAD_GENERATOR' }); +} + const availableCurves = new Set(crypto.getCurves()); const availableHashes = new Set(crypto.getHashes()); diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 7c82e4dd8b97ea..775b752ac3b105 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -909,6 +909,41 @@ if (!process.features.openssl_is_boringssl) { } } } + + const der = publicKey.export({ format: 'der', type: 'spki' }); + const saltLengthParam = Buffer.from([0xa2, 0x03, 0x02, 0x01, 0x10]); + const saltLengthOffset = der.indexOf(saltLengthParam); + assert.notStrictEqual(saltLengthOffset, -1); + + const importMalformedPublicKey = common.mustCall((key) => { + const malformedKey = createPublicKey({ + key, + format: 'der', + type: 'spki' + }); + assert.strictEqual(malformedKey.asymmetricKeyType, 'rsa-pss'); + assert.strictEqual(malformedKey.asymmetricKeyDetails.modulusLength, 2048); + assert.strictEqual(malformedKey.asymmetricKeyDetails.publicExponent, + 65537n); + }, 2); + + { + const negativeSaltLength = Buffer.from(der); + negativeSaltLength[saltLengthOffset + saltLengthParam.length - 1] = 0x80; + importMalformedPublicKey(negativeSaltLength); + } + + { + const oversizedSaltLength = Buffer.concat([ + der.subarray(0, saltLengthOffset), + Buffer.from([0xa2, 0x0b, 0x02, 0x09, 1, 0, 0, 0, 0, 0, 0, 0, 0x10]), + der.subarray(saltLengthOffset + saltLengthParam.length), + ]); + oversizedSaltLength.writeUInt16BE(der.readUInt16BE(2) + 8, 2); + oversizedSaltLength[5] = der[5] + 8; + oversizedSaltLength[18] = der[18] + 8; + importMalformedPublicKey(oversizedSaltLength); + } } { diff --git a/test/parallel/test-http-server-consumed-timeout.js b/test/parallel/test-http-server-consumed-timeout.js index b1781913b56866..683421394a0fad 100644 --- a/test/parallel/test-http-server-consumed-timeout.js +++ b/test/parallel/test-http-server-consumed-timeout.js @@ -7,7 +7,7 @@ const http = require('http'); const durationBetweenIntervals = []; let timeoutTooShort = false; -const TIMEOUT = common.platformTimeout(200); +const TIMEOUT = common.platformTimeout(1000); const INTERVAL = Math.floor(TIMEOUT / 8); runTest(TIMEOUT); diff --git a/test/parallel/test-http2-compat-serverresponse-trailers-streaming.js b/test/parallel/test-http2-compat-serverresponse-trailers-streaming.js new file mode 100644 index 00000000000000..b15cc4680b00bc --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-trailers-streaming.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +// Regression test for the auto-empty-trailers optimization: when the +// response headers are flushed before the response is ended (streaming +// mode), trailers registered afterwards must still be sent, and responses +// that never register trailers must complete normally. + +const server = h2.createServer(); +server.listen(0, common.mustCall(() => { + const port = server.address().port; + server.on('request', (request, response) => { + if (request.url === '/trailers') { + response.writeHead(200); + response.write('hello'); + // Trailers registered after the headers were already flushed. + response.setTrailer('x-checksum', 'abc'); + response.addTrailers({ 'x-count': 2 }); + response.end('world'); + } else { + response.writeHead(200); + response.write('no'); + response.end('trailers'); + } + }); + + const client = h2.connect(`http://localhost:${port}`); + + { + const request = client.request({ ':path': '/trailers' }); + let body = ''; + request.setEncoding('utf8'); + request.on('data', (chunk) => body += chunk); + request.on('trailers', common.mustCall((trailers) => { + assert.strictEqual(trailers['x-checksum'], 'abc'); + assert.strictEqual(trailers['x-count'], '2'); + })); + request.on('end', common.mustCall(() => { + assert.strictEqual(body, 'helloworld'); + maybeClose(); + })); + request.end(); + } + + { + const request = client.request({ ':path': '/plain' }); + let body = ''; + request.setEncoding('utf8'); + request.on('data', (chunk) => body += chunk); + request.on('trailers', common.mustNotCall()); + request.on('end', common.mustCall(() => { + assert.strictEqual(body, 'notrailers'); + maybeClose(); + })); + request.end(); + } + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + client.close(); + server.close(); + } + } +})); diff --git a/test/parallel/test-process-env-allowed-flags-are-documented.js b/test/parallel/test-process-env-allowed-flags-are-documented.js index b592decedd4ccd..6028bbab787e29 100644 --- a/test/parallel/test-process-env-allowed-flags-are-documented.js +++ b/test/parallel/test-process-env-allowed-flags-are-documented.js @@ -133,6 +133,7 @@ assert(undocumented.delete('--experimental-global-customevent')); assert(undocumented.delete('--experimental-global-webcrypto')); assert(undocumented.delete('--experimental-report')); assert(undocumented.delete('--experimental-worker')); +assert(undocumented.delete('--napi-modules')); assert(undocumented.delete('--node-snapshot')); assert(undocumented.delete('--no-node-snapshot')); assert(undocumented.delete('--loader')); diff --git a/test/parallel/test-quic-session-preferred-address-ipv6.mjs b/test/parallel/test-quic-session-preferred-address-ipv6.mjs index e9f23c3bf554d5..35e539848b4aba 100644 --- a/test/parallel/test-quic-session-preferred-address-ipv6.mjs +++ b/test/parallel/test-quic-session-preferred-address-ipv6.mjs @@ -100,6 +100,9 @@ const clientSession = await connect(serverEndpoint.address, { family: 'ipv6', }, }, + maxDatagramSendAttempts: 100, // While the connection is restablished, + // all the acknowledgement packets of ngtcp2 are counted as send attempts + // so either this or a delay, or a change in ngtcp2 interfaces }); await clientSession.opened; diff --git a/test/parallel/test-quic-session-preferred-address.mjs b/test/parallel/test-quic-session-preferred-address.mjs index c4a55ee4d42b74..6a526a8c9ca63f 100644 --- a/test/parallel/test-quic-session-preferred-address.mjs +++ b/test/parallel/test-quic-session-preferred-address.mjs @@ -78,6 +78,9 @@ const clientSession = await connect(serverEndpoint.address, { strictEqual(oldRemote, null); strictEqual(preferred, true); }), + maxDatagramSendAttempts: 100, // While the connection is restablished, + // all the acknowledgement packets of ngtcp2 are counted as send attempts + // so either this or a delay, or a change in ngtcp2 interfaces }); await clientSession.opened; diff --git a/test/parallel/test-quic-session-unobserved-error-close.mjs b/test/parallel/test-quic-session-unobserved-error-close.mjs new file mode 100644 index 00000000000000..c0440766aaaac1 --- /dev/null +++ b/test/parallel/test-quic-session-unobserved-error-close.mjs @@ -0,0 +1,47 @@ +// Flags: --experimental-quic --no-warnings + +// Regression test: destroying a session with an error while its `closed` +// promise is not being observed must NOT surface as an unhandled rejection. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import { subscribe, unsubscribe } from 'node:diagnostics_channel'; +import { setImmediate as tick } from 'node:timers/promises'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// Any unhandled rejection - e.g. an unobserved `closed` rejecting - fails. +process.on('unhandledRejection', mustNotCall('unexpected unhandled rejection')); + +// We use the diagnostics channel to observe the error close without actually +// observing session.closed (not listening is the whole point of the test): +const serverErrored = Promise.withResolvers(); +function onSessionError() { + unsubscribe('quic.session.error', onSessionError); + serverErrored.resolve(); +} +subscribe('quic.session.error', onSessionError); + +// The server session callback is left deliberately empty, so no response +// is sent and closed remains unobserved: +const serverEndpoint = await listen(mustCall()); + +const clientSession = await connect(serverEndpoint.address); +await clientSession.opened; + +// Finish handshake before close: +await tick(); + +// Cleanly close from the client with an error code, so the server +// receives a peer error close: +await clientSession.close({ code: 1 }); + +// Wait until the server has processed the error close, plus another tick +// to ensure unobserved promise rejection doesn't fire anywhere: +await serverErrored.promise; +await tick(); + +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-stream-uni-no-onstream.mjs b/test/parallel/test-quic-stream-uni-no-onstream.mjs new file mode 100644 index 00000000000000..feaa657fb07b53 --- /dev/null +++ b/test/parallel/test-quic-stream-uni-no-onstream.mjs @@ -0,0 +1,47 @@ +// Flags: --experimental-quic --no-warnings +// Test: client-initiated unidirectional stream with no onstream +// The client creates a uni stream with no onstream. +// Verify that this causes no crash. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; + +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { createPrivateKey } = await import('node:crypto'); +const { listen, connect } = await import('../common/quic.mjs'); + +const serverKey = createPrivateKey(readKey('agent1-key.pem')); +const serverCert = readKey('agent1-cert.pem'); + +const done = Promise.withResolvers(); + +const serverEndpoint = await listen(mustCall(async (session) => { + await session.opened; + done.resolve(); +}), { + sni: { '*': { keys: [serverKey], certs: [serverCert] } }, + alpn: ['repro'], +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + alpn: 'repro', + ca: serverCert, +}); + +await clientSession.opened; + +await clientSession.createUnidirectionalStream({ + body: 'something something darkside', +}); + +await done.promise; + +await clientSession.close(); + +await serverEndpoint.close(); diff --git a/test/parallel/test-runner-entry-file.mjs b/test/parallel/test-runner-entry-file.mjs new file mode 100644 index 00000000000000..94750e6a745e50 --- /dev/null +++ b/test/parallel/test-runner-entry-file.mjs @@ -0,0 +1,59 @@ +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { describe, it, run } from 'node:test'; +import assert from 'node:assert'; + +const aPath = fixtures.path('test-runner', 'entry-file', 'a.test.mjs'); +const bPath = fixtures.path('test-runner', 'entry-file', 'b.test.mjs'); +const helperPath = fixtures.path('test-runner', 'entry-file', 'helper.mjs'); + +async function collectEvents(options) { + const events = []; + const stream = run({ files: [aPath, bPath], ...options }); + stream.on('test:fail', () => {}); + for await (const event of stream) { + events.push(event); + } + return events; +} + +describe('entryFile attribution in reporter events', { concurrency: false }, () => { + it('stamps entryFile on events forwarded from child processes', async () => { + const events = await collectEvents({ isolation: 'process' }); + const checked = { __proto__: null, A: 0, B: 0 }; + + for (const { type, data } of events) { + if (data?.name === 'restore A' || data?.name === 'restore B') { + const target = data.name === 'restore A' ? 'A' : 'B'; + const expectedEntry = target === 'A' ? aPath : bPath; + assert.strictEqual(data.file, helperPath, + `${type} file should be the definition site`); + assert.strictEqual(data.entryFile, expectedEntry, + `${type} entryFile should be the entry file`); + checked[target]++; + } + } + + // Each subtest emits at least enqueue/dequeue/start/pass/complete. + assert.ok(checked.A >= 4, `expected events for restore A, got ${checked.A}`); + assert.ok(checked.B >= 4, `expected events for restore B, got ${checked.B}`); + }); + + it('stamps entryFile on top-level tests forwarded from child processes', async () => { + const events = await collectEvents({ isolation: 'process' }); + const pass = events.filter(({ type }) => type === 'test:pass'); + const backupA = pass.find(({ data }) => data.name === 'backup A'); + const backupB = pass.find(({ data }) => data.name === 'backup B'); + assert.strictEqual(backupA.data.entryFile, aPath); + assert.strictEqual(backupB.data.entryFile, bPath); + }); + + it('does not stamp entryFile with isolation none', async () => { + const events = await collectEvents({ isolation: 'none' }); + for (const { data } of events) { + if (data?.name === 'restore A' || data?.name === 'restore B') { + assert.strictEqual(data.entryFile, undefined); + } + } + }); +}); diff --git a/test/parallel/test-runner-v8-deserializer.mjs b/test/parallel/test-runner-v8-deserializer.mjs index 5e50df441da59e..6959b93fb5918c 100644 --- a/test/parallel/test-runner-v8-deserializer.mjs +++ b/test/parallel/test-runner-v8-deserializer.mjs @@ -5,6 +5,7 @@ import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert'; import { finished } from 'node:stream/promises'; import { DefaultSerializer } from 'node:v8'; +import { resolve } from 'node:path'; import serializer from 'internal/test_runner/reporter/v8-serializer'; import runner from 'internal/test_runner/runner'; @@ -14,10 +15,15 @@ async function toArray(chunks) { return arr; } +const entryFile = resolve('filetest'); const diagnosticEvent = { type: 'test:diagnostic', data: { nesting: 0, details: {}, message: 'diagnostic' }, }; +const reportedDiagnosticEvent = { + type: 'test:diagnostic', + data: { ...diagnosticEvent.data, entryFile }, +}; const chunks = await toArray(serializer([diagnosticEvent])); const defaultSerializer = new DefaultSerializer(); defaultSerializer.writeHeader(); @@ -67,28 +73,28 @@ describe('v8 deserializer', common.mustCall(() => { it('should deserialize a chunk with no serialization', async () => { const reported = await collectReported([Buffer.from('unknown')]); assert.deepStrictEqual(reported, [ - { data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, + { data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, ]); }); it('should deserialize a serialized chunk', async () => { const reported = await collectReported(chunks); - assert.deepStrictEqual(reported, [diagnosticEvent]); + assert.deepStrictEqual(reported, [reportedDiagnosticEvent]); }); it('should deserialize a serialized chunk after non-serialized chunk', async () => { const reported = await collectReported([Buffer.concat([Buffer.from('unknown'), ...chunks])]); assert.deepStrictEqual(reported, [ - { data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, - diagnosticEvent, + { data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, + reportedDiagnosticEvent, ]); }); it('should deserialize a serialized chunk before non-serialized output', async () => { const reported = await collectReported([Buffer.concat([ ...chunks, Buffer.from('unknown')])]); assert.deepStrictEqual(reported, [ - diagnosticEvent, - { data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, + reportedDiagnosticEvent, + { data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, ]); }); @@ -131,7 +137,7 @@ describe('v8 deserializer', common.mustCall(() => { oversizedLengthHeader, ...chunks, ]); - assert.deepStrictEqual(reported.at(-1), diagnosticEvent); + assert.deepStrictEqual(reported.at(-1), reportedDiagnosticEvent); assert.strictEqual(reported.filter((event) => event.type === 'test:diagnostic').length, 1); assert.strictEqual(collectStdout(reported), oversizedLengthStdout); }); @@ -152,7 +158,7 @@ describe('v8 deserializer', common.mustCall(() => { const data = chunks[0]; const reported = await collectReported([data.subarray(0, i), data.subarray(i)]); assert.deepStrictEqual(reported, [ - diagnosticEvent, + reportedDiagnosticEvent, ]); }); @@ -163,9 +169,9 @@ describe('v8 deserializer', common.mustCall(() => { Buffer.concat([data.subarray(i), Buffer.from('unknown')]), ]); assert.deepStrictEqual(reported, [ - { data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, - diagnosticEvent, - { data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, + { data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, + reportedDiagnosticEvent, + { data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' }, ]); } ); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index aa7a3a73ae6649..b3a1dc434537c3 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -53,6 +53,29 @@ suite('StatementSync.prototype.get()', () => { const stmt = db.prepare('SELECT 1 as __proto__, 2 as constructor, 3 as toString'); t.assert.deepStrictEqual(stmt.get(), { __proto__: null, ['__proto__']: 1, constructor: 2, toString: 3 }); }); + + test('reflects an added column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'"); + t.assert.deepStrictEqual(stmt.get(), { + __proto__: null, key: 'key1', val: 'val1', extra: 'def', + }); + }); + + test('reflects a dropped column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)'); + db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)') + .run('key1', 'val1', 'x'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec('ALTER TABLE storage DROP COLUMN extra'); + t.assert.deepStrictEqual(stmt.get(), { + __proto__: null, key: 'key1', val: 'val1', + }); + }); }); suite('StatementSync.prototype.all()', () => { @@ -83,6 +106,29 @@ suite('StatementSync.prototype.all()', () => { { __proto__: null, key: 'key2', val: 'val2' }, ]); }); + + test('reflects an added column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'"); + t.assert.deepStrictEqual(stmt.all(), [ + { __proto__: null, key: 'key1', val: 'val1', extra: 'def' }, + ]); + }); + + test('reflects a dropped column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)'); + db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)') + .run('key1', 'val1', 'x'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec('ALTER TABLE storage DROP COLUMN extra'); + t.assert.deepStrictEqual(stmt.all(), [ + { __proto__: null, key: 'key1', val: 'val1' }, + ]); + }); }); suite('StatementSync.prototype.iterate()', () => { @@ -125,6 +171,29 @@ suite('StatementSync.prototype.iterate()', () => { } }); + test('reflects an added column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'"); + t.assert.deepStrictEqual(stmt.iterate().toArray(), [ + { __proto__: null, key: 'key1', val: 'val1', extra: 'def' }, + ]); + }); + + test('reflects a dropped column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)'); + db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)') + .run('key1', 'val1', 'x'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec('ALTER TABLE storage DROP COLUMN extra'); + t.assert.deepStrictEqual(stmt.iterate().toArray(), [ + { __proto__: null, key: 'key1', val: 'val1' }, + ]); + }); + test('iterator keeps the prepared statement from being collected', (t) => { const db = new DatabaseSync(':memory:'); db.exec(` diff --git a/test/parallel/test-stream-iter-consumers-merge.js b/test/parallel/test-stream-iter-consumers-merge.js index b551599f731462..859fcf8c1580fa 100644 --- a/test/parallel/test-stream-iter-consumers-merge.js +++ b/test/parallel/test-stream-iter-consumers-merge.js @@ -170,6 +170,32 @@ async function testMergeSignalDuringPendingMultiSourceRead() { await assert.rejects(next, { name: 'AbortError' }); } +async function testMergeDoesNotDrainSourcesWhileIdle() { + function source(n) { + return { + __proto__: null, + pulls: 0, + async *[Symbol.asyncIterator]() { + while (this.pulls < n) { + yield [Buffer.from(`${++this.pulls}`)]; + } + }, + }; + } + + const a = source(5); + const b = source(5); + const iterator = merge(a, b)[Symbol.asyncIterator](); + + await iterator.next(); + await new Promise(setImmediate); + + assert.strictEqual(a.pulls, 1); + assert.strictEqual(b.pulls, 1); + + await iterator.return?.(); +} + // merge() accepts string sources (normalized via from()) async function testMergeStringSources() { const batches = []; @@ -306,6 +332,7 @@ Promise.all([ testMergeConsumerBreak(), testMergeSignalMidIteration(), testMergeSignalDuringPendingMultiSourceRead(), + testMergeDoesNotDrainSourcesWhileIdle(), testMergeStringSources(), testMergeObjectLikeSources(), testMergeCleanupErrorOnly(), diff --git a/test/parallel/test-stream-iter-from-sync.js b/test/parallel/test-stream-iter-from-sync.js index d3a5cf671e10d8..50d85dc94a98b7 100644 --- a/test/parallel/test-stream-iter-from-sync.js +++ b/test/parallel/test-stream-iter-from-sync.js @@ -3,7 +3,7 @@ const common = require('../common'); const assert = require('assert'); -const { fromSync } = require('stream/iter'); +const { fromSync, textSync } = require('stream/iter'); function testFromSyncString() { // String input should be UTF-8 encoded @@ -186,6 +186,30 @@ function testFromSyncRejectsAsyncIterable() { assert.throws(() => fromSync(gen()), { code: 'ERR_INVALID_ARG_TYPE' }); } +function testFromSyncPrefersIteratorForDualIterable() { + const input = { + *[Symbol.iterator]() { + yield new TextEncoder().encode('sync'); + }, + async *[Symbol.asyncIterator]() { + yield new TextEncoder().encode('async'); + }, + }; + + assert.strictEqual(textSync(fromSync(input)), 'sync'); +} + +function testFromSyncPrefersIteratorForThenableIterable() { + const input = { + then() {}, + *[Symbol.iterator]() { + yield new TextEncoder().encode('sync'); + }, + }; + + assert.strictEqual(textSync(fromSync(input)), 'sync'); +} + // Promise rejected function testFromSyncRejectsPromise() { assert.throws(() => fromSync(Promise.resolve('hello')), @@ -232,6 +256,8 @@ Promise.all([ testFromSyncTopLevelProtocolOverIterator(), testFromSyncIgnoresAsyncStreamable(), testFromSyncRejectsAsyncIterable(), + testFromSyncPrefersIteratorForDualIterable(), + testFromSyncPrefersIteratorForThenableIterable(), testFromSyncRejectsPromise(), testFromSyncDataView(), ]).then(common.mustCall()); diff --git a/test/parallel/test-tls-check-server-identity.js b/test/parallel/test-tls-check-server-identity.js index 4863fca55165ee..e21f39ccc76341 100644 --- a/test/parallel/test-tls-check-server-identity.js +++ b/test/parallel/test-tls-check-server-identity.js @@ -100,6 +100,27 @@ const tests = [ cert: { subject: { CN: '8.8.8.8' }, subjectaltname: 'IP Address:8.8.8.8' } }, + // An "IP Address:" SAN also matches an IPv6 host. Regression test for the + // IDNA-normalization change: domainToASCII('::1') === '' (an IPv6 literal is + // not a domain), which made the normalized host skip IPv6 IP-SAN matching. + { + host: '::1', + cert: { subject: {}, subjectaltname: 'IP Address:::1' } + }, + + // IPv6 hosts and SANs are matched canonically. + { + host: '2001:db8::1', + cert: { subject: {}, subjectaltname: 'IP Address:2001:DB8:0:0:0:0:0:1' } + }, + + // A non-matching IPv6 "IP Address:" SAN is rejected. + { + host: '::1', + cert: { subject: {}, subjectaltname: 'IP Address:::2' }, + error: 'IP: ::1 is not in the cert\'s list: ::2' + }, + // But not when it's a CIDR. { host: '8.8.8.8', diff --git a/test/parallel/test-tls-connect-secure-context.js b/test/parallel/test-tls-connect-secure-context.js index a0d9170c20904f..442973118a563f 100644 --- a/test/parallel/test-tls-connect-secure-context.js +++ b/test/parallel/test-tls-connect-secure-context.js @@ -23,6 +23,9 @@ connect({ return cleanup(); })); +// Invalid dhparam input is silently discarded for compatibility. +tls.createSecureContext({ dhparam: fixtures.readKey('ec-key.pem') }); + connect({ client: { servername: 'agent1', diff --git a/test/parallel/test-vm-global-restricted-property.js b/test/parallel/test-vm-global-restricted-property.js new file mode 100644 index 00000000000000..e57cc8dc854613 --- /dev/null +++ b/test/parallel/test-vm-global-restricted-property.js @@ -0,0 +1,20 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const vm = require('vm'); + +const ctx = vm.createContext({}); + +// Define a non-configurable ("restricted") property on the context global. +vm.runInContext( + "Object.defineProperty(this, 'foo', { value: 1, configurable: false });", + ctx +); + +// https://tc39.es/ecma262/#sec-globaldeclarationinstantiation 3.d: +// If hasRestrictedGlobal is true, throw a SyntaxError exception. +assert.throws( + () => vm.runInContext('let foo = 2;', ctx), + vm.runInContext('SyntaxError', ctx), +); diff --git a/test/parallel/test-webstreams-queue-wraparound.js b/test/parallel/test-webstreams-queue-wraparound.js new file mode 100644 index 00000000000000..15ffc59689b981 --- /dev/null +++ b/test/parallel/test-webstreams-queue-wraparound.js @@ -0,0 +1,178 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +// Regression test for the ring-buffer [[queue]] backing store used by the +// WHATWG stream controllers: exercises growth, wrap-around, drain-rewind +// and shrink behavior through the public API only. + +async function testDeepDefaultQueue() { + // Queue 3000 chunks before reading anything: the backing ring must grow + // (and re-linearize) repeatedly, then fully drain (shrink on rewind). + const rs = new ReadableStream({ + start(controller) { + for (let i = 0; i < 3000; i++) + controller.enqueue(i); + controller.close(); + }, + }, { highWaterMark: Infinity }); + + const reader = rs.getReader(); + for (let i = 0; i < 3000; i++) { + const { value, done } = await reader.read(); + assert.strictEqual(done, false); + assert.strictEqual(value, i); + } + const { done } = await reader.read(); + assert.strictEqual(done, true); +} + +async function testInterleavedWraparound() { + // Interleave enqueues and reads so head/tail wrap around the ring many + // times without ever growing it, including repeated empty transitions. + let enqueued = 0; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + }); + + const reader = rs.getReader(); + let expected = 0; + for (let round = 0; round < 500; round++) { + const burst = (round % 3) + 1; + for (let i = 0; i < burst; i++) + controller.enqueue(enqueued++); + for (let i = 0; i < burst; i++) { + const { value, done } = await reader.read(); + assert.strictEqual(done, false); + assert.strictEqual(value, expected++); + } + } + assert.strictEqual(expected, enqueued); +} + +async function testCustomSizesSurviveQueueing() { + // Fractional and zero sizes must round-trip through the queue exactly + // (desiredSize accounting depends on the stored per-chunk size). + const sizes = [0.25, 0, 1.75, 3, 0.5]; + const chunks = ['a', 'b', 'c', 'd', 'e']; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + }, { + highWaterMark: 10, + size(chunk) { + return sizes[chunks.indexOf(chunk)]; + }, + }); + + let expectedDesired = 10; + for (let i = 0; i < chunks.length; i++) { + controller.enqueue(chunks[i]); + expectedDesired -= sizes[i]; + assert.strictEqual(controller.desiredSize, expectedDesired); + } + const reader = rs.getReader(); + for (let i = 0; i < chunks.length; i++) { + const { value } = await reader.read(); + assert.strictEqual(value, chunks[i]); + expectedDesired += sizes[i]; + assert.strictEqual(controller.desiredSize, expectedDesired); + } +} + +async function testByteQueueUnevenReads() { + // Enqueue 7-byte chunks but read through 4-byte BYOB views: the byte + // queue's head record is partially consumed in place across reads. + const total = 7 * 300; + let written = 0; + const rs = new ReadableStream({ + type: 'bytes', + pull(controller) { + if (written >= total) { + controller.close(); + return; + } + const chunk = new Uint8Array(7); + for (let i = 0; i < 7; i++) + chunk[i] = (written + i) % 251; + written += 7; + controller.enqueue(chunk); + }, + }); + + const reader = rs.getReader({ mode: 'byob' }); + let read = 0; + while (read < total) { + const { value, done } = await reader.read(new Uint8Array(4)); + if (done) break; + for (let i = 0; i < value.length; i++) { + assert.strictEqual(value[i], (read + i) % 251, + `byte ${read + i} mismatch`); + } + read += value.length; + } + assert.strictEqual(read, total); +} + +async function testDeepByteQueueBurst() { + // Fill the byte queue with 2500 records before the default reader + // drains it, forcing ring growth and the drain-to-empty rewind. + const rs = new ReadableStream({ + type: 'bytes', + start(controller) { + for (let i = 0; i < 2500; i++) + controller.enqueue(new Uint8Array([i & 0xff])); + controller.close(); + }, + }); + + const reader = rs.getReader(); + for (let i = 0; i < 2500; i++) { + const { value, done } = await reader.read(); + assert.strictEqual(done, false); + assert.strictEqual(value[0], i & 0xff); + } + const { done } = await reader.read(); + assert.strictEqual(done, true); +} + +async function testWritableQueueBackpressureDrain() { + // Buffer thousands of writes behind a slow sink, then let it drain: + // the writable controller queue grows, wraps, and empties while every + // chunk is delivered in order. + const received = []; + let release; + const gate = new Promise((resolve) => { release = resolve; }); + const ws = new WritableStream({ + async write(chunk) { + if (received.length === 0) await gate; + received.push(chunk); + }, + }, { highWaterMark: 1 }); + + const writer = ws.getWriter(); + const writes = []; + for (let i = 0; i < 2000; i++) + writes.push(writer.write(i)); + release(); + await writer.close(); + await Promise.all(writes); + assert.strictEqual(received.length, 2000); + for (let i = 0; i < 2000; i++) + assert.strictEqual(received[i], i); +} + +(async () => { + await testDeepDefaultQueue(); + await testInterleavedWraparound(); + await testCustomSizesSurviveQueueing(); + await testByteQueueUnevenReads(); + await testDeepByteQueueBurst(); + await testWritableQueueBackpressureDrain(); +})().then(common.mustCall()); diff --git a/test/parallel/test-worker-broadcastchannel.js b/test/parallel/test-worker-broadcastchannel.js index 12535271c15596..89bccd1fbddf19 100644 --- a/test/parallel/test-worker-broadcastchannel.js +++ b/test/parallel/test-worker-broadcastchannel.js @@ -147,7 +147,7 @@ assert.throws(() => new BroadcastChannel(), { const bc1 = new BroadcastChannel('channel4'); const bc2 = new BroadcastChannel('channel4'); bc1.postMessage('some data'); - assert.strictEqual(receiveMessageOnPort(bc2).message, 'some data'); + assert.strictEqual(receiveMessageOnPort(bc2).message.value, 'some data'); assert.strictEqual(receiveMessageOnPort(bc2), undefined); bc1.close(); bc2.close(); @@ -183,3 +183,32 @@ assert.throws(() => new BroadcastChannel(), { "BroadcastChannel { name: 'channel5', active: false }" ); } + +{ + const bc = new BroadcastChannel('worker-source'); + + new Worker(` + const assert = require('assert'); + const { BroadcastChannel, threadId } = require('worker_threads'); + + const bc = new BroadcastChannel('worker-source'); + + bc.onmessage = (evt) => { + assert.strictEqual(evt.data, 'from-main'); + assert.strictEqual(evt.source, 0); + + bc.close(); + }; + + bc.postMessage('from-worker'); + `, { eval: true }); + + bc.onmessage = common.mustCall((evt) => { + assert.strictEqual(evt.data, 'from-worker'); + + assert.ok(evt.source > 0); + + bc.postMessage('from-main'); + bc.close(); + }); +} diff --git a/test/parallel/test-worker-stack-overflow-stack-size.js b/test/parallel/test-worker-stack-overflow-stack-size.js index 481ff55100ac76..8aef401ab68ca5 100644 --- a/test/parallel/test-worker-stack-overflow-stack-size.js +++ b/test/parallel/test-worker-stack-overflow-stack-size.js @@ -8,6 +8,14 @@ const { Worker } = require('worker_threads'); // Verify that Workers don't care about --stack-size, as they have their own // fixed and known stack sizes. +// The depth of the stack depends not only on the stack size, but also on the size of each +// stack frame, which in turn depends on which tier the recursive function happens to be +// running at when the overflow occurs. Under load the background tier-up can land at a +// non-deterministic point in the recursion and flake the test. Keep the recursive +// function in the interpreter with %NeverOptimizeFunction() so the frame size - +// and thus the depth - is deterministic. +v8.setFlagsFromString('--allow-natives-syntax'); + async function runWorker(options = {}) { const empiricalStackDepth = new Uint32Array(new SharedArrayBuffer(4)); const worker = new Worker(` @@ -16,6 +24,7 @@ async function runWorker(options = {}) { empiricalStackDepth[0]++; f(); } + %NeverOptimizeFunction(f); f();`, { eval: true, workerData: { empiricalStackDepth },