From 13eee46e7d003dc452b04aea3a1f26d162227c19 Mon Sep 17 00:00:00 2001 From: Shuyun Yang Date: Tue, 27 Oct 2026 20:08:55 -0700 Subject: [PATCH] [Fix] `nvm_ls_cached`: match dots in the version literally, and ignore a trailing dot The pattern reached `grep` as a regex, so its dots matched any character: `20.1.1` also matched a cached `v20.111.1`, which `nvm install --offline 20.1.1` could then pick as the newest match. A trailing dot, as in `20.1.`, likewise matched `v20.10.0` but not `v20.1.x`. A bare `v`, `iojs-`, or `iojs-v` now matches every cached node or io.js version respectively. --- nvm.sh | 8 +- .../nvm_ls_cached matches version components | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100755 test/fast/Unit tests/nvm_ls_cached matches version components diff --git a/nvm.sh b/nvm.sh index 352c7af7..8943c3dc 100755 --- a/nvm.sh +++ b/nvm.sh @@ -4483,7 +4483,13 @@ nvm_ls_cached() { local SUFFIX SUFFIX="${NVM_OS}-${NVM_ARCH}" local NVM_CACHED_PATTERN - NVM_CACHED_PATTERN="$(nvm_ensure_version_prefix "${PATTERN:-.*}")" + NVM_CACHED_PATTERN="$(nvm_ensure_version_prefix "${PATTERN%.}")" + # match dots literally, so that `20.1.1` does not also match `v20.111.1` + case "${NVM_CACHED_PATTERN}" in + '' | v) NVM_CACHED_PATTERN='.*' ;; + iojs- | iojs-v) NVM_CACHED_PATTERN='iojs-v.*' ;; + *) NVM_CACHED_PATTERN="$(nvm_echo "${NVM_CACHED_PATTERN}" | command sed 's/[][\.*^$]/\\&/g')" ;; + esac # a release, like v27.0.0, is not one of its alphas local NVM_CACHED_RELEASE NVM_CACHED_RELEASE='v-' diff --git a/test/fast/Unit tests/nvm_ls_cached matches version components b/test/fast/Unit tests/nvm_ls_cached matches version components new file mode 100755 index 00000000..4575aff3 --- /dev/null +++ b/test/fast/Unit tests/nvm_ls_cached matches version components @@ -0,0 +1,73 @@ +#!/bin/sh + +die () { echo "$@" ; cleanup ; exit 1; } + +cleanup() { + [ -z "${TEST_DIR-}" ] || rm -rf "${TEST_DIR}" +} + +\. ../../../nvm.sh +\. ../../common.sh + +TEST_DIR="$(mktemp -d)" || die 'unable to create temporary cache' +NVM_DIR="${TEST_DIR}" +CACHE_DIR="$(nvm_cache_dir)" +SUFFIX="$(nvm_get_os)-$(nvm_get_arch)" +mkdir -p "${NVM_DIR}/alias" "${NVM_DIR}/versions/node" + +for VERSION in v2.1.1 v20.1.1 v20.1.10 v20.10.0 v20.111.1; do + mkdir -p "${CACHE_DIR}/bin/node-${VERSION}-${SUFFIX}" + mkdir -p "${CACHE_DIR}/src/node-${VERSION}" +done +for VERSION in v2.1.1 v2.1.10 v2.10.0; do + mkdir -p "${CACHE_DIR}/bin/iojs-${VERSION}-${SUFFIX}" + mkdir -p "${CACHE_DIR}/src/iojs-${VERSION}" +done + +assert_cached() { + try nvm_ls_cached "$1" + [ "${CAPTURED_EXIT_CODE}" = 0 ] || die "nvm_ls_cached '$1' failed: ${CAPTURED_EXIT_CODE}" + [ "${CAPTURED_STDOUT}" = "$2" ] || die "nvm_ls_cached '$1': expected >$2<, got >${CAPTURED_STDOUT}<" +} + +# A major or minor prefix must end at a version component boundary. +assert_cached '20' 'v20.1.1 +v20.1.10 +v20.10.0 +v20.111.1' +assert_cached '20.1' 'v20.1.1 +v20.1.10' +assert_cached '20.1.' 'v20.1.1 +v20.1.10' +assert_cached '2' 'iojs-v2.1.1 +v2.1.1 +iojs-v2.1.10 +iojs-v2.10.0' + +# A complete version must not select a longer patch or treat dots as wildcards. +assert_cached '20.1.1' 'v20.1.1' +assert_cached 'v20.1.1' 'v20.1.1' +assert_cached '20.1.2' '' +assert_cached 'iojs-2.1' 'iojs-v2.1.1 +iojs-v2.1.10' +assert_cached 'iojs-v2.1.1' 'iojs-v2.1.1' + +# Preserve the unfiltered and io.js-only forms. +ALL_VERSIONS="$(nvm_ls_cached)" +assert_cached 'v' "${ALL_VERSIONS}" +assert_cached 'iojs-' 'iojs-v2.1.1 +iojs-v2.1.10 +iojs-v2.10.0' +assert_cached 'iojs-v' 'iojs-v2.1.1 +iojs-v2.1.10 +iojs-v2.10.0' + +# The offline installer uses this resolver when no installed version matches. +try nvm_offline_version '20.1' +[ "${CAPTURED_STDOUT}" = 'v20.1.10' ] && [ "${CAPTURED_EXIT_CODE}" = 0 ] \ + || die "nvm_offline_version '20.1' should resolve v20.1.10, got ${CAPTURED_STDOUT} (${CAPTURED_EXIT_CODE})" +try nvm_offline_version '20.1.1' +[ "${CAPTURED_STDOUT}" = 'v20.1.1' ] && [ "${CAPTURED_EXIT_CODE}" = 0 ] \ + || die "nvm_offline_version '20.1.1' should resolve v20.1.1, got ${CAPTURED_STDOUT} (${CAPTURED_EXIT_CODE})" + +cleanup