diff --git a/tests/test_api.py b/tests/test_api.py index 5f7ddfaa38..939434fbbe 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -37,7 +37,8 @@ def setUpModule(): Root, Snapshot, Timestamp, - Targets + Targets, + TargetInfo ) from securesystemslib.interface import ( @@ -324,15 +325,12 @@ def test_metadata_targets(self): "sha512": "ef5beafa16041bcdd2937140afebd485296cd54f7348ecd5a4d035c09759608de467a7ac0eb58753d0242df873c305e8bffad2454aa48f44480f15efae1cacd0" }, - fileinfo = { - 'hashes': hashes, - 'length': 28 - } + fileinfo = TargetInfo(length=28, hashes=hashes) # Assert that data is not aleady equal self.assertNotEqual(targets.signed.targets[filename], fileinfo) # Update an already existing fileinfo - targets.signed.update(filename, fileinfo) + targets.signed.update(filename, fileinfo.to_dict()) # Verify that data is updated self.assertEqual(targets.signed.targets[filename], fileinfo) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 380ddf092b..8d1eb581d9 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -574,6 +574,58 @@ def update( self.meta[metadata_fn] = MetadataInfo(version, length, hashes) +class TargetInfo: + """A container with information about a particular target file. + Instances of TargetInfo are used as values in a dictionary + called "targets" in Targets. + + Attributes: + length: An integer indicating the length of the target file. + hashes: A dictionary containing hash algorithms and the + hashes resulted from applying them over the target file:: + + 'hashes': { + '': '', + '': '', + ... + } + + custom: An optional dictionary which may include version numbers, + dependencies, or any other data that the application wants + to include to describe the target file:: + + 'custom': { + 'type': 'metadata', + 'file_permissions': '0644', + ... + } // optional + + """ + + def __init__(self, length: int, hashes: JsonDict, + custom: Optional[JsonDict] = None) -> None: + self.length = length + self.hashes = hashes + self.custom = custom + + + def __eq__(self, other: 'TargetInfo') -> bool: + """Compare objects by their values instead of by their addresses.""" + return (self.length == other.length and + self.hashes == other.hashes and + self.custom == other.custom) + + + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + json_dict = {'length': self.length, 'hashes': self.hashes} + + if self.custom is not None: + json_dict['custom'] = self.custom + + return json_dict + + class Targets(Signed): """A container for the signed part of targets metadata. @@ -581,15 +633,7 @@ class Targets(Signed): targets: A dictionary that contains information about target files:: { - '': { - 'length': , - 'hashes': { - '': '', - '': '', - ... - }, - 'custom': // optional - }, + '': , ... } @@ -633,20 +677,38 @@ class Targets(Signed): # pylint: disable=too-many-arguments def __init__( self, _type: str, version: int, spec_version: str, - expires: datetime, targets: JsonDict, delegations: JsonDict - ) -> None: + expires: datetime, targets: Dict[str, TargetInfo], + delegations: JsonDict) -> None: super().__init__(_type, version, spec_version, expires) - # TODO: Add class for meta self.targets = targets + + # TODO: Add Key and Role classes self.delegations = delegations + @classmethod + def from_dict(cls, signed_dict: JsonDict) -> 'Targets': + """Creates Targets object from its JSON/dict representation. """ + + # signed is passed by reference, make sure we are not changing it. + changed_signed = signed_dict.copy() + for target_path in signed_dict['targets'].keys(): + changed_signed['targets'][target_path] = TargetInfo( + **signed_dict['targets'][target_path]) + + return super().from_dict(changed_signed) + + # Serialization. def to_dict(self) -> JsonDict: """Returns the JSON-serializable dictionary representation of self. """ json_dict = super().to_dict() + target_dict = {} + for target_path, target_file_obj in self.targets.items(): + target_dict[target_path] = target_file_obj.to_dict() + json_dict.update({ - 'targets': self.targets, + 'targets': target_dict, 'delegations': self.delegations, }) return json_dict @@ -654,4 +716,4 @@ def to_dict(self) -> JsonDict: # Modification. def update(self, filename: str, fileinfo: JsonDict) -> None: """Assigns passed target file info to meta dict. """ - self.targets[filename] = fileinfo + self.targets[filename] = TargetInfo(**fileinfo)