Skip to content

Commit

Permalink
Implement glob-like pattern matching
Browse files Browse the repository at this point in the history
According to the recently updated version of the specification the shell
style wildcard matching is glob-like (see theupdateframework/specification#174),
and therefore a path separator in a path should not be matched by a
wildcard in the PATHPATTERN.

That's not what happens with `fnmatch.fnmatch()` which doesn't
see "/" separator as a special symbol.
For example: fnmatch.fnmatch("targets/foo.tgz", "*.tgz") will return
True which is not what glob-like implementation will do.

We should make sure that target_path and the pathpattern contain the
same number of directories and because each part of the pathpattern
could include a glob pattern we should check that fnmatch.fnmatch() is
true on each target and pathpattern directory fragment separated by "/".

Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
  • Loading branch information
MVrachev committed Aug 25, 2021
1 parent 6063054 commit 122c8af
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 4 deletions.
30 changes: 28 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
MetaFile,
TargetFile,
Delegations,
DelegatedRole,
_is_target_in_pathpattern
)

from tuf.api.serialization import (
Expand All @@ -40,7 +40,6 @@

from tuf.api.serialization.json import (
JSONSerializer,
JSONDeserializer,
CanonicalJSONSerializer
)

Expand Down Expand Up @@ -465,6 +464,33 @@ def test_metadata_root(self):
with self.assertRaises(KeyError):
root.signed.remove_key('root', 'nosuchkey')

def test_is_target_in_pathpattern(self):
supported_use_cases = [
("foo.tgz", "foo.tgz"),
("foo.tgz", "*"),
("foo.tgz", "*.tgz"),
("foo-version-a.tgz", "foo-version-?.tgz"),
("targets/foo.tgz", "targets/*.tgz"),
("foo/bar/zoo/k.tgz", "foo/bar/zoo/*"),
("foo/bar/zoo/k.tgz", "foo/*/zoo/*"),
("foo/bar/zoo/k.tgz", "*/*/*/*"),
("foo/bar", "f?o/bar"),
("foo/bar", "*o/bar"),
]
for targetpath, pathpattern in supported_use_cases:
self.assertTrue(_is_target_in_pathpattern(targetpath, pathpattern))

invalid_use_cases = [
("targets/foo.tgz", "*.tgz"),
("/foo.tgz", "*.tgz",),
("targets/foo.tgz", "*"),
("foo-version-alpha.tgz", "foo-version-?.tgz"),
("foo//bar", "*/bar"),
("foo/bar", "f?/bar")
]
for targetpath, pathpattern in invalid_use_cases:
self.assertFalse(_is_target_in_pathpattern(targetpath, pathpattern))


def test_delegation_class(self):
# empty keys and roles
Expand Down
26 changes: 24 additions & 2 deletions tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,27 @@ def update(self, rolename: str, role_info: MetaFile) -> None:
self.meta[metadata_fn] = role_info


def _is_target_in_pathpattern(targetpath: str, pathpattern: str) -> bool:
"""Determines whether "targetname" matches the "pathpattern"."""
# We need to make sure that targetname and pathpattern are pointing to the
# same directory as fnmatch doesn't threat "/" as a special symbol. Example:
# fnmatch.fnmatch("targets/foo.tgz", "*.tgz") will return True.
target_parts = targetpath.split("/")
pattern_parts = pathpattern.split("/")
if len(target_parts) != len(pattern_parts):
# Difference in the number of nested dirs means that target_dir and
# pathpattern_dir point to different places.
return False

# Every part in the pathpattern could include a glob pattern, that's why
# each of the target parts should match the corresponding pathpattern part.
for target_dir, pattern_dir in zip(target_parts, pattern_parts):
if not fnmatch.fnmatch(target_dir, pattern_dir):
return False

return True


class DelegatedRole(Role):
"""A container with information about a delegated role.
Expand Down Expand Up @@ -1077,8 +1098,9 @@ def is_delegated_path(self, target_filepath: str) -> bool:
# are also considered matches. Make sure to strip any leading
# path separators so that a match is made.
# Example: "foo.tgz" should match with "/*.tgz".
if fnmatch.fnmatch(
target_filepath.lstrip(os.sep), pathpattern.lstrip(os.sep)
if _is_target_in_pathpattern(
target_filepath.lstrip(os.sep),
pathpattern.lstrip(os.sep),
):
return True

Expand Down

0 comments on commit 122c8af

Please sign in to comment.