Skip to content

Commit

Permalink
Add HashDelegations class
Browse files Browse the repository at this point in the history
Add HashDelegations class containing the information from the
succinct_hash_delegation dict described in TAP 15.
This allows for easy mypy checks on the types, easy enforcement on
TAP 15 restrictions (as for example that "delegation_hash_prefix_len"
must be between 1 and 32) and support for unrecognized fields
inside succinct_hash_delegation without much of a hassle.

Signed-off-by: Martin Vrachev <[email protected]>
  • Loading branch information
MVrachev committed Apr 12, 2022
1 parent 6f5fd73 commit 1d85351
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 17 deletions.
14 changes: 12 additions & 2 deletions tests/test_metadata_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,14 @@ def test_snapshot_serialization(self, test_case_data: str) -> None:
"terminating": false, "threshold": 1}',
"ordered keyids": '{"keyids": ["keyid2", "keyid1"], "name": "a", "paths": ["fn1", "fn2"], \
"terminating": false, "threshold": 1}',
# "succinct_hash_delegation with unrecognized fieldse": '{"keyids": ["keyid"], "terminating": false, \
# "threshold": 99, "succinct_hash_delegations": {"delegation_hash_prefix_len": 8, "bin_name_prefix": "foo", "foo": "bar"}}',
}

@utils.run_sub_tests_with_dataset(valid_delegated_roles)
def test_delegated_role_serialization(self, test_case_data: str) -> None:
case_dict = json.loads(test_case_data)
deserialized_role = DelegatedRole.from_dict(copy.copy(case_dict))
deserialized_role = DelegatedRole.from_dict(copy.deepcopy(case_dict))
self.assertDictEqual(case_dict, deserialized_role.to_dict())

invalid_delegated_roles: utils.DataSet = {
Expand All @@ -413,14 +415,22 @@ def test_delegated_role_serialization(self, test_case_data: str) -> None:
"succinct_hash_delegations": {"bin_name_prefix" : "foo"}}',
"missing bin_name_prefix from succinct_hash_delegations": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false, \
"succinct_hash_delegations": {"delegation_hash_prefix_len" : 8}}',
"succinct_hash_delegation with invalid delegation_hash_prefix_len type": '{"keyids": ["keyid"], "terminating": false, \
"threshold": 99, "succinct_hash_delegations": {"delegation_hash_prefix_len": "a", "bin_name_prefix": "foo"}}',
"succinct_hash_delegation with invalid bin_name_prefix type": '{"keyids": ["keyid"], "terminating": false, \
"threshold": 99, "succinct_hash_delegations": {"delegation_hash_prefix_len": 8, "bin_name_prefix": 1}}',
"succinct_hash_delegation with high delegation_hash_prefix_len value": '{"keyids": ["keyid"], "terminating": false, \
"threshold": 99, "succinct_hash_delegations": {"delegation_hash_prefix_len": 50, "bin_name_prefix": "foo"}}',
"succinct_hash_delegation with low delegation_hash_prefix_len value": '{"keyids": ["keyid"], "terminating": false, \
"threshold": 99, "succinct_hash_delegations": {"delegation_hash_prefix_len": 0, "bin_name_prefix": "foo"}}',
}

@utils.run_sub_tests_with_dataset(invalid_delegated_roles)
def test_invalid_delegated_role_serialization(
self, test_case_data: str
) -> None:
case_dict = json.loads(test_case_data)
with self.assertRaises(ValueError):
with self.assertRaises((ValueError, TypeError)):
DelegatedRole.from_dict(case_dict)

invalid_delegations: utils.DataSet = {
Expand Down
86 changes: 71 additions & 15 deletions tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,64 @@ def to_dict(self) -> Dict[str, Any]:
return snapshot_dict


class HashDelegations:
"""A container with information about a succinct hash delegation.
Args:
hash_prefix_len: Number of bits between 1 and 32 used to separate the
different bins.
bin_name_prefix: Prefix of all bin names.
Raises:
ValueError, TypeError: Invalid arguments.
"""

def __init__(
self,
hash_prefix_len: int,
bin_name_prefix: str,
unrecognized_fields: Optional[Mapping[str, Any]] = None,
) -> None:
if hash_prefix_len <= 0 or hash_prefix_len > 32:
raise ValueError("hash_prefix_len must be between 1 and 32")
if not isinstance(bin_name_prefix, str):
raise ValueError("bin_name_prefix must be a string")

self.hash_prefix_len = hash_prefix_len
self.bin_name_prefix = bin_name_prefix
self.unrecognized_fields: Mapping[str, Any] = unrecognized_fields or {}

def __eq__(self, other: Any) -> bool:
if not isinstance(other, HashDelegations):
return False

return (
self.hash_prefix_len == other.hash_prefix_len
and self.bin_name_prefix == other.bin_name_prefix
and self.unrecognized_fields == other.unrecognized_fields
)

@classmethod
def from_dict(cls, succinct_hash_info: Dict[str, Any]) -> "HashDelegations":
"""Creates ``HashDelegations`` object from its json/dict representation.
Raises:
ValueError, TypeError: Invalid arguments.
"""
hash_prefix_len = succinct_hash_info.pop("delegation_hash_prefix_len")
bin_name_prefix = succinct_hash_info.pop("bin_name_prefix")
# All fields left in the succinct_hash_info are unrecognized.
return cls(hash_prefix_len, bin_name_prefix, succinct_hash_info)

def to_dict(self) -> Dict[str, Any]:
"""Returns the dict representation of self."""
return {
"delegation_hash_prefix_len": self.hash_prefix_len,
"bin_name_prefix": self.bin_name_prefix,
**self.unrecognized_fields,
}


class DelegatedRole(Role):
"""A container with information about a delegated role.
Expand Down Expand Up @@ -1321,19 +1379,17 @@ def __init__(
not isinstance(p, str) for p in path_hash_prefixes
):
raise ValueError("Path_hash_prefixes must be strings")

succinct_info = None
if succinct_hash_info is not None:
if not isinstance(succinct_hash_info, Dict):
raise ValueError("succinct_hash_info must be a dict")
if "delegation_hash_prefix_len" not in succinct_hash_info:
raise ValueError(
"succinct_hash_info requires delegation_hash_prefix_len"
)
if "bin_name_prefix" not in succinct_hash_info:
raise ValueError("succinct_hash_info requires bin_name_prefix")

succinct_info = HashDelegations.from_dict(succinct_hash_info)

self.paths = paths
self.path_hash_prefixes = path_hash_prefixes
self.succinct_hash_info = succinct_hash_info
self.succinct_hash_info = succinct_info

def __eq__(self, other: Any) -> bool:
if not isinstance(other, DelegatedRole):
Expand Down Expand Up @@ -1377,19 +1433,19 @@ def from_dict(cls, role_dict: Dict[str, Any]) -> "DelegatedRole":
def to_dict(self) -> Dict[str, Any]:
"""Returns the dict representation of self."""
base_role_dict = super().to_dict()
res_dict: Dict[str, Any] = {
res: Dict[str, Any] = {
"terminating": self.terminating,
**base_role_dict,
}
if self.name is not None:
res_dict["name"] = self.name
res["name"] = self.name
if self.paths is not None:
res_dict["paths"] = self.paths
res["paths"] = self.paths
elif self.path_hash_prefixes is not None:
res_dict["path_hash_prefixes"] = self.path_hash_prefixes
res["path_hash_prefixes"] = self.path_hash_prefixes
elif self.succinct_hash_info is not None:
res_dict["succinct_hash_delegations"] = self.succinct_hash_info
return res_dict
res["succinct_hash_delegations"] = self.succinct_hash_info.to_dict()
return res

@staticmethod
def _is_target_in_pathpattern(targetpath: str, pathpattern: str) -> bool:
Expand Down Expand Up @@ -1421,11 +1477,11 @@ def _calculate_delegation(self, hash_bits_representation: str) -> str:
"succinct_hash_info must be set to calculate the delegation"
)

bit_length = self.succinct_hash_info["delegation_hash_prefix_len"]
bit_length = self.succinct_hash_info.hash_prefix_len
# Get the first bit_length of bits and then cast them to decimal.
bin_number = int(hash_bits_representation[:bit_length], 2)

name_prefix = self.succinct_hash_info["bin_name_prefix"]
name_prefix = self.succinct_hash_info.bin_name_prefix
return f"{name_prefix}-{bin_number}"

def find_delegation(self, target_filepath: str) -> Optional[str]:
Expand Down

0 comments on commit 1d85351

Please sign in to comment.