From e72cab46d5db79b4b78cb2021876b3ef0cb2049e Mon Sep 17 00:00:00 2001 From: Alan Date: Thu, 1 Oct 2026 08:51:01 +0800 Subject: [PATCH 1/2] Fix symlink prefixes for multiple globstar captures --- tests/test_globmatch.py | 10 ++++++++++ tests/test_globmatch_prefix.py | 28 ++++++++++++++++++++++++++++ wcmatch/_wcmatch.py | 4 +--- 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 tests/test_globmatch_prefix.py diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py index 3acb6d7..9c1cbdf 100644 --- a/tests/test_globmatch.py +++ b/tests/test_globmatch.py @@ -1655,6 +1655,16 @@ def test_globmatch_trigger_symlink_cache(self): self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/{*.txt,*.t*}', flags=self.default_flags)) + def test_globmatch_multiple_globstars(self): + """Check symlinks in each globstar relative to its own prefix.""" + + self.mktemp('outer', 'literal', 'target', 'file.txt') + self.mksymlink('target', self.norm('outer', 'literal', 'link')) + filename = self.tempdir + '/outer/literal/link/file.txt' + pattern = '**/literal/**/file.txt' + self.assertFalse(glob.globmatch(filename, pattern, flags=self.default_flags)) + self.assertTrue(glob.globmatch(filename, pattern, flags=self.default_flags | glob.L)) + def test_globmatch_globstarlong(self): """Test `***`.""" diff --git a/tests/test_globmatch_prefix.py b/tests/test_globmatch_prefix.py new file mode 100644 index 0000000..9b8b1a5 --- /dev/null +++ b/tests/test_globmatch_prefix.py @@ -0,0 +1,28 @@ +"""Test independent filesystem prefixes for multiple globstars.""" +import os +import pytest +from wcmatch import glob + + +@pytest.mark.parametrize('as_bytes', [False, True]) +@pytest.mark.parametrize('pattern', ['**/literal/**/file.txt', '**/literal/**/leaf/**/file.txt']) +@pytest.mark.parametrize('link', ['outer/literal/link', 'outer/link']) +def test_globstar_symlink_prefix(tmp_path, monkeypatch, as_bytes, pattern, link): + """Only symlinks on the actual matched path should prevent traversal.""" + + filename = 'outer/literal/link/leaf/final/file.txt' + file = tmp_path / filename + file.parent.mkdir(parents=True) + file.write_text('') + root = str(tmp_path) + link_path = os.path.normpath(str(tmp_path / link)) + if as_bytes: + filename = os.fsencode(filename) + pattern = os.fsencode(pattern) + root = os.fsencode(root) + link_path = os.fsencode(link_path) + + monkeypatch.setattr('wcmatch._wcmatch.os.path.islink', lambda path: os.path.normpath(path) == link_path) + flags = glob.GLOBSTAR | glob.REALPATH + assert glob.globmatch(filename, pattern, root_dir=root, flags=flags) == (link == 'outer/link') + assert glob.globmatch(filename, pattern, root_dir=root, flags=flags | glob.FOLLOW) diff --git a/wcmatch/_wcmatch.py b/wcmatch/_wcmatch.py index 03cbf6a..ce6fde7 100644 --- a/wcmatch/_wcmatch.py +++ b/wcmatch/_wcmatch.py @@ -91,7 +91,6 @@ def _fs_match( strip = (RE_WIN_STRIP if is_win else RE_STRIP)[self.ptype] # type: Any end = len(filename) - 1 - base = None m = pattern.fullmatch(filename) if m: matched = True @@ -103,8 +102,7 @@ def _fs_match( if star: at_end = m.end(i) == end parts = split.split(star.strip(strip)) - if base is None: - base = os.path.join(root, filename[:m.start(i)]) + base = os.path.join(root, filename[:m.start(i)]) last_part = len(parts) for j, part in enumerate(parts, 1): base = os.path.join(base, part) From fb974801c163d45ec1648ed90f75a15febc83b80 Mon Sep 17 00:00:00 2001 From: Alan Date: Thu, 1 Oct 2026 09:44:42 +0800 Subject: [PATCH 2/2] Address review feedback on globmatch regression tests --- tests/test_globmatch.py | 26 +++++++++++++++++++++++++- tests/test_globmatch_prefix.py | 28 ---------------------------- 2 files changed, 25 insertions(+), 29 deletions(-) delete mode 100644 tests/test_globmatch_prefix.py diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py index 9c1cbdf..1b79db3 100644 --- a/tests/test_globmatch.py +++ b/tests/test_globmatch.py @@ -58,6 +58,30 @@ def skip_unless_symlink(test): return test if ok else unittest.skip(msg)(test) +@pytest.mark.parametrize('as_bytes', [False, True]) +@pytest.mark.parametrize('pattern', ['**/literal/**/file.txt', '**/literal/**/leaf/**/file.txt']) +@pytest.mark.parametrize('link', ['outer/literal/link', 'outer/link']) +def test_globstar_symlink_prefix(tmp_path, monkeypatch, as_bytes, pattern, link): + """Only symlinks on the actual matched path should prevent traversal.""" + + filename = 'outer/literal/link/leaf/final/file.txt' + file = tmp_path / filename + file.parent.mkdir(parents=True) + file.write_text('') + root = str(tmp_path) + link_path = os.path.normpath(str(tmp_path / link)) + if as_bytes: + filename = os.fsencode(filename) + pattern = os.fsencode(pattern) + root = os.fsencode(root) + link_path = os.fsencode(link_path) + + monkeypatch.setattr('wcmatch._wcmatch.os.path.islink', lambda path: os.path.normpath(path) == link_path) + flags = glob.GLOBSTAR | glob.REALPATH + assert glob.globmatch(filename, pattern, root_dir=root, flags=flags) == (link == 'outer/link') + assert glob.globmatch(filename, pattern, root_dir=root, flags=flags | glob.FOLLOW) + + class _TestGlobmatch(unittest.TestCase): """Test the `WcMatch` class.""" @@ -1656,7 +1680,7 @@ def test_globmatch_trigger_symlink_cache(self): self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/{*.txt,*.t*}', flags=self.default_flags)) def test_globmatch_multiple_globstars(self): - """Check symlinks in each globstar relative to its own prefix.""" + """Check symlinks in each `globstar` relative to its own prefix.""" self.mktemp('outer', 'literal', 'target', 'file.txt') self.mksymlink('target', self.norm('outer', 'literal', 'link')) diff --git a/tests/test_globmatch_prefix.py b/tests/test_globmatch_prefix.py deleted file mode 100644 index 9b8b1a5..0000000 --- a/tests/test_globmatch_prefix.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Test independent filesystem prefixes for multiple globstars.""" -import os -import pytest -from wcmatch import glob - - -@pytest.mark.parametrize('as_bytes', [False, True]) -@pytest.mark.parametrize('pattern', ['**/literal/**/file.txt', '**/literal/**/leaf/**/file.txt']) -@pytest.mark.parametrize('link', ['outer/literal/link', 'outer/link']) -def test_globstar_symlink_prefix(tmp_path, monkeypatch, as_bytes, pattern, link): - """Only symlinks on the actual matched path should prevent traversal.""" - - filename = 'outer/literal/link/leaf/final/file.txt' - file = tmp_path / filename - file.parent.mkdir(parents=True) - file.write_text('') - root = str(tmp_path) - link_path = os.path.normpath(str(tmp_path / link)) - if as_bytes: - filename = os.fsencode(filename) - pattern = os.fsencode(pattern) - root = os.fsencode(root) - link_path = os.fsencode(link_path) - - monkeypatch.setattr('wcmatch._wcmatch.os.path.islink', lambda path: os.path.normpath(path) == link_path) - flags = glob.GLOBSTAR | glob.REALPATH - assert glob.globmatch(filename, pattern, root_dir=root, flags=flags) == (link == 'outer/link') - assert glob.globmatch(filename, pattern, root_dir=root, flags=flags | glob.FOLLOW)