From b845ad31f4186c331eb5d868930eb4da9b159e74 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Wed, 25 Nov 2020 18:06:54 +0200 Subject: [PATCH] New API: add TargetFile class Add a new TargetFile class to tuf.api.metadata module make Targets class to use it. This class will contain information about the "targets" field from targets.json Also, update the tests for that change. Signed-off-by: Martin Vrachev --- tests/test_api.py | 10 ++--- tuf/api/metadata.py | 90 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 20 deletions(-) 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 4c52089f36..77e66bcc52 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -572,6 +572,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. @@ -579,15 +631,7 @@ class Targets(Signed): targets: A dictionary that contains information about target files:: { - '': { - 'length': , - 'hashes': { - '': '', - '': '', - ... - }, - 'custom': // optional - }, + '': , ... } @@ -631,20 +675,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_dict 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 @@ -652,4 +714,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)