Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tests/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pickle
import stat
import sys
import time
import unittest

import jaraco.itertools
Expand Down Expand Up @@ -545,6 +546,19 @@ def test_glob_empty(self):
with self.assertRaises(ValueError):
root.glob('')

def test_glob_many_stars(self):
"""
A pattern with many ``*`` in one segment must not backtrack
exponentially against a non-matching name.
"""
zf = zipfile.ZipFile(io.BytesIO(), 'w')
zf.writestr('a' * 40, b'')
root = zipfile.Path(zf)
pattern = '*a' * 25 + '.txt'
start = time.monotonic()
assert list(root.glob(pattern)) == []
assert time.monotonic() - start < 2

@pass_alpharep
def test_eq_hash(self, alpharep):
root = zipfile.Path(alpharep)
Expand Down
67 changes: 57 additions & 10 deletions zipp/glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,66 @@ def translate_core(self, pattern):
'.*/[^/][^/]*'
"""
self.restrict_rglob(pattern)
return ''.join(map(self.replace, separate(self.star_not_empty(pattern))))
return self.assemble(self.tokenize(self.star_not_empty(pattern)))

def replace(self, match):
def tokenize(self, pattern):
"""
Perform the replacements for a match from :func:`separate`.
Yield (greedy, regex) pairs for each piece of pattern, where
greedy marks a ``*`` whose ``[^/]*`` may backtrack against a
neighboring ``*`` in the same path segment.
"""
return match.group('set') or (
re
.escape(match.group(0))
.replace('\\*\\*', r'.*')
.replace('\\*', rf'[^{re.escape(self.seps)}]*')
.replace('\\?', r'[^/]')
)
for match in separate(pattern):
captured = match.group('set')
if captured:
yield False, captured
continue
text = match.group(0)
i = 0
while i < len(text):
if text.startswith('**', i):
yield False, r'.*'
i += 2
elif text[i] == '*':
yield True, rf'[^{re.escape(self.seps)}]*'
i += 1
elif text[i] == '?':
yield False, r'[^/]'
i += 1
else:
yield False, re.escape(text[i])
i += 1

def assemble(self, tokens):
"""
Join translated tokens into a regex.

When two ``*`` stars sit in the same path segment, the leading
one is committed with an atomic group so that ``*a*a*...`` can no
longer backtrack exponentially against a non-matching name.
"""
tokens = list(tokens)
res = []
i = 0
while i < len(tokens):
greedy, regex = tokens[i]
if not greedy:
res.append(regex)
i += 1
continue
j = i + 1
fixed = []
while j < len(tokens) and not tokens[j][0] and '/' not in tokens[j][1]:
fixed.append(tokens[j][1])
j += 1
if j < len(tokens) and tokens[j][0]:
name = f'g{len(res)}'
res.append(f'(?=(?P<{name}>{regex}?{"".join(fixed)}))(?P={name})')
i = j
else:
res.append(regex)
res.extend(fixed)
i = j
return ''.join(res)

def restrict_rglob(self, pattern):
"""
Expand Down