Skip to content

Commit

Permalink
New API: add MetadataInfo class
Browse files Browse the repository at this point in the history
Add MetaMetaFileFile class to tuf.api.metadata module.
This class is be used for the "meta" field in Timestamp and
Snapshot metadata files described the specification.

Signed-off-by: Martin Vrachev <[email protected]>
  • Loading branch information
MVrachev committed Dec 14, 2020
1 parent dde89c2 commit c8b5474
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 55 deletions.
28 changes: 15 additions & 13 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def setUpModule():
import tuf.exceptions
from tuf.api.metadata import (
Metadata,
MetadataInfo,
Root,
Snapshot,
Timestamp,
Expand Down Expand Up @@ -224,17 +225,18 @@ def test_metadata_snapshot(self):
# Create a dict representing what we expect the updated data to be
fileinfo = copy.deepcopy(snapshot.signed.meta)
hashes = {'sha256': 'c2986576f5fdfd43944e2b19e775453b96748ec4fe2638a6d2f32f1310967095'}
fileinfo['role1.json']['version'] = 2
fileinfo['role1.json']['hashes'] = hashes
fileinfo['role1.json']['length'] = 123
fileinfo['role1.json'].version = 2
fileinfo['role1.json'].hashes = hashes
fileinfo['role1.json'].length = 123


self.assertNotEqual(snapshot.signed.meta, fileinfo)
snapshot.signed.update('role1', 2, 123, hashes)
self.assertEqual(snapshot.signed.meta, fileinfo)

# Update only version. Length and hashes are optional.
snapshot.signed.update('role1', 3)
fileinfo['role1.json'] = {'version': 3}
fileinfo['role1.json'] = MetadataInfo(3)
self.assertEqual(snapshot.signed.meta, fileinfo)


Expand Down Expand Up @@ -263,18 +265,18 @@ def test_metadata_timestamp(self):
self.assertEqual(timestamp.signed.expires, datetime(2036, 1, 3, 0, 0))

hashes = {'sha256': '0ae9664468150a9aa1e7f11feecb32341658eb84292851367fea2da88e8a58dc'}
fileinfo = copy.deepcopy(timestamp.signed.meta['snapshot.json'])
fileinfo['hashes'] = hashes
fileinfo['version'] = 2
fileinfo['length'] = 520

self.assertNotEqual(timestamp.signed.meta['snapshot.json'], fileinfo)
fileinfo = copy.deepcopy(timestamp.signed.meta)
fileinfo['snapshot.json'].hashes = hashes
fileinfo['snapshot.json'].version = 2
fileinfo['snapshot.json'].length = 520
self.assertNotEqual(timestamp.signed.meta, fileinfo)
timestamp.signed.update(2, 520, hashes)
self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo)
self.assertEqual(timestamp.signed.meta, fileinfo)

# Update only version. Length and hashes are optional.
timestamp.signed.update(3)
fileinfo = {'version': 3}
self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo)
fileinfo['snapshot.json'] = MetadataInfo(version=3)
self.assertEqual(timestamp.signed.meta, fileinfo)


def test_metadata_root(self):
Expand Down
128 changes: 86 additions & 42 deletions tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,50 @@ def remove_key(self, role: str, keyid: str) -> None:
del self.keys[keyid]


class MetadataInfo:
"""A container with information about a particular metadata file.
Instances of MetadataInfo are used as values in a dictionary called
"meta" in Timestamp and Snapshot.
Attributes:
version: An integer indicating the version of the metadata file.
length: An optional integer indicating the length of the metadata file.
hashes: A optional dictionary containing hash algorithms and the
hashes resulting from applying them over the metadata file.::
'hashes': {
'<HASH ALGO 1>': '<METADATA FILE HASH 1>',
'<HASH ALGO 2>': '<METADATA FILE HASH 2>',
...
}
"""

def __init__(self, version: int, length: Optional[int] = None,
hashes: Optional[JsonDict] = None) -> None:
self.version = version
self.length = length
self.hashes = hashes


def __eq__(self, other: 'MetadataInfo') -> bool:
"""Compare objects by their values instead of by their addresses."""
return (self.version == other.version and
self.length == other.length and
self.hashes == other.hashes)


def to_dict(self) -> JsonDict:
"""Returns the JSON-serializable dictionary representation of self. """
json_dict = {'version': self.version}

if self.length is not None:
json_dict['length'] = self.length

if self.hashes is not None:
json_dict['hashes'] = self.hashes

return json_dict


class Timestamp(Signed):
Expand All @@ -427,23 +471,14 @@ class Timestamp(Signed):
meta: A dictionary that contains information about snapshot metadata::
{
'snapshot.json': {
'version': <SNAPSHOT METADATA VERSION NUMBER>,
'length': <SNAPSHOT METADATA FILE SIZE>, // optional
'hashes': {
'<HASH ALGO 1>': '<SNAPSHOT METADATA FILE HASH 1>',
'<HASH ALGO 2>': '<SNAPSHOT METADATA FILE HASH 2>',
...
} // optional
}
'snapshot.json': <MetadataInfo INSTANCE>
}
"""
def __init__(
self, _type: str, version: int, spec_version: str,
expires: datetime, meta: JsonDict) -> None:
expires: datetime, meta: Dict[str, MetadataInfo]) -> None:
super().__init__(_type, version, spec_version, expires)
# TODO: Add class for meta
self.meta = meta


Expand All @@ -452,22 +487,31 @@ def to_dict(self) -> JsonDict:
"""Returns the JSON-serializable dictionary representation of self. """
json_dict = super().to_dict()
json_dict.update({
'meta': self.meta
'meta': {
'snapshot.json': self.meta['snapshot.json'].to_dict()
}
})
return json_dict


@classmethod
def from_dict(cls, signed_dict: JsonDict) -> 'Timestamp':
"""Creates Timestamp object from its JSON/dict representation. """

# signed_dict is passed by reference, make sure we are not changing it.
changed_signed = signed_dict.copy()
changed_signed['meta']['snapshot.json'] = MetadataInfo(
**signed_dict['meta']['snapshot.json'])

return super().from_dict(changed_signed)


# Modification.
def update(self, version: int, length: Optional[int] = None,
hashes: Optional[JsonDict] = None) -> None:
"""Assigns passed info about snapshot metadata to meta dict. """

self.meta['snapshot.json'] = {'version': version}
if length is not None:
self.meta['snapshot.json']['length'] = length

if hashes is not None:
self.meta['snapshot.json']['hashes'] = hashes
self.meta['snapshot.json'] = MetadataInfo(version, length, hashes)


class Snapshot(Signed):
Expand All @@ -477,38 +521,43 @@ class Snapshot(Signed):
meta: A dictionary that contains information about targets metadata::
{
'targets.json': {
'version': <TARGETS METADATA VERSION NUMBER>,
'length': <TARGETS METADATA FILE SIZE>, // optional
'hashes': {
'<HASH ALGO 1>': '<TARGETS METADATA FILE HASH 1>',
'<HASH ALGO 2>': '<TARGETS METADATA FILE HASH 2>',
...
} // optional
},
'<DELEGATED TARGETS ROLE 1>.json': {
...
},
'<DELEGATED TARGETS ROLE 2>.json': {
...
},
'targets.json': <MetadataInfo INSTANCE>,
'<DELEGATED TARGETS ROLE 1>.json': <MetadataInfo INSTANCE>,
'<DELEGATED TARGETS ROLE 2>.json': <MetadataInfo INSTANCE>,
...
}
"""
def __init__(
self, _type: str, version: int, spec_version: str,
expires: datetime, meta: JsonDict) -> None:
expires: datetime, meta: Dict[str, MetadataInfo]) -> None:
super().__init__(_type, version, spec_version, expires)
# TODO: Add class for meta
self.meta = meta


@classmethod
def from_dict(cls, signed_dict: JsonDict) -> 'Snapshot':
"""Creates Snapshot 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 meta_path in signed_dict['meta'].keys():
changed_signed['meta'][meta_path] = MetadataInfo(
**signed_dict['meta'][meta_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()
meta_dict = {}
for meta_path, meta_info in self.meta.items():
meta_dict[meta_path] = meta_info.to_dict()

json_dict.update({
'meta': self.meta
'meta': meta_dict
})
return json_dict

Expand All @@ -520,12 +569,7 @@ def update(
"""Assigns passed (delegated) targets role info to meta dict. """
metadata_fn = f'{rolename}.json'

self.meta[metadata_fn] = {'version': version}
if length is not None:
self.meta[metadata_fn]['length'] = length

if hashes is not None:
self.meta[metadata_fn]['hashes'] = hashes
self.meta[metadata_fn] = MetadataInfo(version, length, hashes)


class Targets(Signed):
Expand Down

0 comments on commit c8b5474

Please sign in to comment.