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..7aba7d2 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 @@ -33,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 @@ -201,6 +210,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)