Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New metadata API: add MetadataInfo and TargetFile classes #1223

Closed
10 changes: 4 additions & 6 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def setUpModule():
Root,
Snapshot,
Timestamp,
Targets
Targets,
TargetInfo
)

from securesystemslib.interface import (
Expand Down Expand Up @@ -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)

Expand Down
88 changes: 74 additions & 14 deletions tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,22 +570,66 @@ 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::
Comment on lines +624 to +625
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the above suggestion, I'd welcome feedback on whether this is clearer or not?

Suggested change
hashes: A dictionary containing hash algorithms and the
hashes resulted from applying them over the target file::
hashes: A dictionary mapping hash algorithms to the
hashes resulting from applying them over the metadata file
contents::


'hashes': {
'<HASH ALGO 1>': '<TARGET FILE HASH 1>',
'<HASH ALGO 2>': '<TARGET FILE HASH 2>',
...
}

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."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about identity here.

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.

Attributes:
targets: A dictionary that contains information about target files::

{
'<TARGET FILE NAME>': {
'length': <TARGET FILE SIZE>,
'hashes': {
'<HASH ALGO 1>': '<TARGET FILE HASH 1>',
'<HASH ALGO 2>': '<TARGETS FILE HASH 2>',
...
},
'custom': <CUSTOM OPAQUE DICT> // optional
},
'<TARGET FILE NAME>': <TargetInfo INSTANCE>,
...
}

Expand Down Expand Up @@ -629,25 +673,41 @@ 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. """
for target_path in signed_dict['targets'].keys():
signed_dict['targets'][target_path] = TargetInfo(
**signed_dict['targets'][target_path])

return super().from_dict(signed_dict)


# 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

# 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['length'],
fileinfo['hashes'], fileinfo.get('custom'))