From 860a9c3a1d8873bf23c2cf2a74827f34781a3cf8 Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Thu, 13 Aug 2026 21:44:06 +0300 Subject: [PATCH 1/2] fix(nodeenv): don't pass tarfile-only filter= to ZipFile.extractall download_node_src() gated filter="data" on the Python version alone, but that argument only exists on tarfile.TarFile.extractall. On Windows and Cygwin the archive is a zip, so every install on Python 3.12+ crashed with: TypeError: ZipFile.extractall() got an unexpected keyword argument 'filter' Gate it on the archive type as well. The tar path keeps filter="data". --- nodeenv.py | 3 ++- tests/nodeenv_test.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/nodeenv.py b/nodeenv.py index e2cf1f0..3439369 100644 --- a/nodeenv.py +++ b/nodeenv.py @@ -637,7 +637,8 @@ def download_node_src(node_url, src_dir, args): for member in members(archive) if re.match(rexp_string, member_name(member)) is None ] - if sys.version_info >= (3, 12): + # filter= is a tarfile-only argument, zipfile has no such option + if sys.version_info >= (3, 12) and not (is_WIN or is_CYGWIN): archive.extractall(src_dir, extract_list, filter="data") else: archive.extractall(src_dir, extract_list) diff --git a/tests/nodeenv_test.py b/tests/nodeenv_test.py index 818c182..88449e7 100644 --- a/tests/nodeenv_test.py +++ b/tests/nodeenv_test.py @@ -6,11 +6,13 @@ from pipes import quote as _quote else: from shlex import quote as _quote +import io import os.path import subprocess import sys import sysconfig import platform +import zipfile try: from unittest import mock @@ -201,6 +203,54 @@ def test__download_node_file(): assert m_urlopen.call_count == 5 +def _zip_with_node(node_version): + buf = io.BytesIO() + with zipfile.ZipFile(buf, 'w') as zf: + zf.writestr('node-v%s-win-x64/README.md' % node_version, 'readme') + zf.writestr('node-v%s-win-x64/node.exe' % node_version, 'binary') + return io.BytesIO(buf.getvalue()) + + +def test_download_node_src_zip(tmpdir): + """On Windows the archive is a zip, which has no extractall(filter=...)""" + class args: + node = '22.14.0' + + with mock.patch.object(nodeenv, 'is_WIN', True), \ + mock.patch.object(nodeenv, '_download_node_file', + return_value=_zip_with_node(args.node)): + nodeenv.download_node_src('https://dummy/node.zip', + tmpdir.strpath, args) + + node_dir = os.path.join(tmpdir.strpath, 'node-v22.14.0-win-x64') + assert os.path.exists(os.path.join(node_dir, 'node.exe')) + # docs are excluded from the extract list + assert not os.path.exists(os.path.join(node_dir, 'README.md')) + + +def test_download_node_src_tar_keeps_data_filter(tmpdir): + """The tar path must keep filter='data' (CVE-2007-4559 protection)""" + class args: + node = '22.14.0' + + archive = mock.MagicMock() + archive.__enter__.return_value = archive + archive.getmembers.return_value = [] + with mock.patch.object(nodeenv, 'is_WIN', False), \ + mock.patch.object(nodeenv, 'is_CYGWIN', False), \ + mock.patch.object(nodeenv, 'tarfile_open', return_value=archive), \ + mock.patch.object(nodeenv, '_download_node_file', + return_value=io.BytesIO(b'')): + nodeenv.download_node_src('https://dummy/node.tar.gz', + tmpdir.strpath, args) + + if sys.version_info >= (3, 12): + archive.extractall.assert_called_once_with( + tmpdir.strpath, [], filter="data") + else: + archive.extractall.assert_called_once_with(tmpdir.strpath, []) + + def test_parse_version(): assert nodeenv.parse_version("v21.7") == (21, 7) assert nodeenv.parse_version("v21.7.3") == (21, 7, 3) From d9083deeb0eae0e053d3716df3283188273665f8 Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Thu, 13 Aug 2026 21:44:27 +0300 Subject: [PATCH 2/2] fix(tests): check the Windows layout in test_smoke test_smoke always sourced nenv/bin/activate with sh, but on Windows nodeenv installs into Scripts/ and writes activate.bat/Activate.ps1 - there is no posix activate script, so sh exited 1 and the test failed on every Windows runner. Run Scripts/node.exe directly there, keeping the install itself covered. --- tests/nodeenv_test.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/nodeenv_test.py b/tests/nodeenv_test.py index 88449e7..7aba7d2 100644 --- a/tests/nodeenv_test.py +++ b/tests/nodeenv_test.py @@ -35,10 +35,17 @@ def test_smoke(tmpdir): '-m', 'nodeenv', '--prebuilt', nenv_path, ]) assert os.path.exists(nenv_path) - activate = _quote(os.path.join(nenv_path, 'bin', 'activate')) - subprocess.check_call([ - 'sh', '-c', '. {} && node --version'.format(activate), - ]) + if sys.platform == 'win32': + # on Windows nodeenv installs into Scripts/ and provides + # activate.bat/Activate.ps1, there is no posix activate script + subprocess.check_call([ + os.path.join(nenv_path, 'Scripts', 'node.exe'), '--version', + ]) + else: + activate = _quote(os.path.join(nenv_path, 'bin', 'activate')) + subprocess.check_call([ + 'sh', '-c', '. {} && node --version'.format(activate), + ]) @pytest.mark.integration