diff --git a/tests/test_api.py b/tests/test_api.py index 2cf1222884..18f80d1a46 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,7 +13,6 @@ import shutil import tempfile import unittest -import copy from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta @@ -40,7 +39,6 @@ from tuf.api.serialization.json import ( JSONSerializer, - JSONDeserializer, CanonicalJSONSerializer ) @@ -465,6 +463,37 @@ 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( + DelegatedRole._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( + DelegatedRole._is_target_in_pathpattern(targetpath, pathpattern) + ) + def test_delegation_class(self): # empty keys and roles diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 01c306100a..ad0d7254c6 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1057,6 +1057,24 @@ def to_dict(self) -> Dict[str, Any]: res_dict["path_hash_prefixes"] = self.path_hash_prefixes return res_dict + @staticmethod + 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. + target_parts = targetpath.split("/") + pattern_parts = pathpattern.split("/") + if len(target_parts) != len(pattern_parts): + return False + + # Every part in the pathpattern could include a glob pattern, that's why + # each of the target and pathpattern parts should match. + for target_dir, pattern_dir in zip(target_parts, pattern_parts): + if not fnmatch.fnmatch(target_dir, pattern_dir): + return False + + return True + def is_delegated_path(self, target_filepath: str) -> bool: """Determines whether the given 'target_filepath' is in one of the paths that DelegatedRole is trusted to provide""" @@ -1079,7 +1097,7 @@ 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( + if self._is_target_in_pathpattern( target_filepath.lstrip(os.sep), pathpattern.lstrip(os.sep) ): return True