Skip to content

Commit b6558c2

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1367 from MVrachev/improvements
Make hashes, length and delegations optional + improvements
2 parents f736028 + 139bfc0 commit b6558c2

File tree

2 files changed

+89
-50
lines changed

2 files changed

+89
-50
lines changed

tests/test_api.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import tuf.exceptions
2424
from tuf.api.metadata import (
2525
Metadata,
26+
Root,
2627
Snapshot,
2728
Timestamp,
2829
Targets,
@@ -98,6 +99,7 @@ def tearDownClass(cls):
9899

99100
def test_generic_read(self):
100101
for metadata, inner_metadata_cls in [
102+
('root', Root),
101103
('snapshot', Snapshot),
102104
('timestamp', Timestamp),
103105
('targets', Targets)]:
@@ -144,7 +146,7 @@ def test_compact_json(self):
144146

145147

146148
def test_read_write_read_compare(self):
147-
for metadata in ['snapshot', 'timestamp', 'targets']:
149+
for metadata in ['root', 'snapshot', 'timestamp', 'targets']:
148150
path = os.path.join(self.repo_dir, 'metadata', metadata + '.json')
149151
metadata_obj = Metadata.from_file(path)
150152

@@ -258,6 +260,18 @@ def test_metadata_snapshot(self):
258260
snapshot.signed.update('role1', 2, 123, hashes)
259261
self.assertEqual(snapshot.signed.meta, fileinfo)
260262

263+
# Update only version. Length and hashes are optional.
264+
snapshot.signed.update('role2', 3)
265+
fileinfo['role2.json'] = {'version': 3}
266+
self.assertEqual(snapshot.signed.meta, fileinfo)
267+
268+
# Test from_dict and to_dict without hashes and length.
269+
snapshot_dict = snapshot.to_dict()
270+
test_dict = snapshot_dict['signed'].copy()
271+
del test_dict['meta']['role1.json']['length']
272+
del test_dict['meta']['role1.json']['hashes']
273+
snapshot = Snapshot.from_dict(test_dict)
274+
self.assertEqual(snapshot_dict['signed'], snapshot.to_dict())
261275

262276
def test_metadata_timestamp(self):
263277
timestamp_path = os.path.join(
@@ -293,6 +307,18 @@ def test_metadata_timestamp(self):
293307
timestamp.signed.update(2, 520, hashes)
294308
self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo)
295309

310+
# Test from_dict and to_dict without hashes and length.
311+
timestamp_dict = timestamp.to_dict()
312+
test_dict = timestamp_dict['signed'].copy()
313+
del test_dict['meta']['snapshot.json']['length']
314+
del test_dict['meta']['snapshot.json']['hashes']
315+
timestamp_test = Timestamp.from_dict(test_dict)
316+
self.assertEqual(timestamp_dict['signed'], timestamp_test.to_dict())
317+
318+
# Update only version. Length and hashes are optional.
319+
timestamp.signed.update(3)
320+
fileinfo = {'version': 3}
321+
self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo)
296322

297323
def test_key_class(self):
298324
keys = {
@@ -419,6 +445,14 @@ def test_metadata_targets(self):
419445
# Verify that data is updated
420446
self.assertEqual(targets.signed.targets[filename], fileinfo)
421447

448+
# Test from_dict/to_dict Targets without delegations
449+
targets_dict = targets.to_dict()
450+
del targets_dict["signed"]["delegations"]
451+
tmp_dict = targets_dict["signed"].copy()
452+
targets_obj = Targets.from_dict(tmp_dict)
453+
tar_d = targets_obj.to_dict()
454+
self.assertEqual(targets_dict["signed"], targets_obj.to_dict())
455+
422456
def setup_dict_with_unrecognized_field(self, file_path, field, value):
423457
json_dict = {}
424458
with open(file_path) as f:

tuf/api/metadata.py

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -511,16 +511,19 @@ class Root(Signed):
511511
supports consistent snapshots.
512512
keys: A dictionary that contains a public key store used to verify
513513
top level roles metadata signatures::
514-
{
515-
'<KEYID>': <Key instance>,
516-
...
517-
},
514+
515+
{
516+
'<KEYID>': <Key instance>,
517+
...
518+
},
519+
518520
roles: A dictionary that contains a list of signing keyids and
519521
a signature threshold for each top level role::
520-
{
521-
'<ROLE>': <Role istance>,
522-
...
523-
}
522+
523+
{
524+
'<ROLE>': <Role istance>,
525+
...
526+
}
524527
525528
"""
526529

@@ -612,7 +615,7 @@ class Timestamp(Signed):
612615
'<HASH ALGO 1>': '<SNAPSHOT METADATA FILE HASH 1>',
613616
'<HASH ALGO 2>': '<SNAPSHOT METADATA FILE HASH 2>',
614617
...
615-
}
618+
} // optional
616619
}
617620
}
618621
@@ -648,14 +651,19 @@ def to_dict(self) -> Dict[str, Any]:
648651

649652
# Modification.
650653
def update(
651-
self, version: int, length: int, hashes: Mapping[str, Any]
654+
self,
655+
version: int,
656+
length: Optional[int] = None,
657+
hashes: Optional[Mapping[str, Any]] = None,
652658
) -> None:
653659
"""Assigns passed info about snapshot metadata to meta dict."""
654-
self.meta["snapshot.json"] = {
655-
"version": version,
656-
"length": length,
657-
"hashes": hashes,
658-
}
660+
self.meta["snapshot.json"] = {"version": version}
661+
662+
if length is not None:
663+
self.meta["snapshot.json"]["length"] = length
664+
665+
if hashes is not None:
666+
self.meta["snapshot.json"]["hashes"] = hashes
659667

660668

661669
class Snapshot(Signed):
@@ -755,34 +763,34 @@ class Targets(Signed):
755763
roles and public key store used to verify their metadata
756764
signatures::
757765
758-
{
759-
'keys' : {
760-
'<KEYID>': {
761-
'keytype': '<KEY TYPE>',
762-
'scheme': '<KEY SCHEME>',
763-
'keyid_hash_algorithms': [
764-
'<HASH ALGO 1>',
765-
'<HASH ALGO 2>'
766-
...
767-
],
768-
'keyval': {
769-
'public': '<PUBLIC KEY HEX REPRESENTATION>'
770-
}
766+
{
767+
'keys' : {
768+
'<KEYID>': {
769+
'keytype': '<KEY TYPE>',
770+
'scheme': '<KEY SCHEME>',
771+
'keyid_hash_algorithms': [
772+
'<HASH ALGO 1>',
773+
'<HASH ALGO 2>'
774+
...
775+
],
776+
'keyval': {
777+
'public': '<PUBLIC KEY HEX REPRESENTATION>'
778+
}
779+
},
780+
...
771781
},
782+
'roles': [
783+
{
784+
'name': '<ROLENAME>',
785+
'keyids': ['<SIGNING KEY KEYID>', ...],
786+
'threshold': <SIGNATURE THRESHOLD>,
787+
'terminating': <TERMINATING BOOLEAN>,
788+
'path_hash_prefixes': ['<HEX DIGEST>', ... ], // or
789+
'paths' : ['PATHPATTERN', ... ],
790+
},
772791
...
773-
},
774-
'roles': [
775-
{
776-
'name': '<ROLENAME>',
777-
'keyids': ['<SIGNING KEY KEYID>', ...],
778-
'threshold': <SIGNATURE THRESHOLD>,
779-
'terminating': <TERMINATING BOOLEAN>,
780-
'path_hash_prefixes': ['<HEX DIGEST>', ... ], // or
781-
'paths' : ['PATHPATTERN', ... ],
782-
},
783-
...
784-
]
785-
}
792+
]
793+
}
786794
787795
"""
788796

@@ -798,7 +806,7 @@ def __init__(
798806
spec_version: str,
799807
expires: datetime,
800808
targets: Mapping[str, Any],
801-
delegations: Mapping[str, Any],
809+
delegations: Optional[Mapping[str, Any]] = None,
802810
unrecognized_fields: Optional[Mapping[str, Any]] = None,
803811
) -> None:
804812
super().__init__(version, spec_version, expires, unrecognized_fields)
@@ -811,19 +819,16 @@ def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
811819
"""Creates Targets object from its dict representation."""
812820
common_args = cls._common_fields_from_dict(targets_dict)
813821
targets = targets_dict.pop("targets")
814-
delegations = targets_dict.pop("delegations")
822+
delegations = targets_dict.pop("delegations", None)
815823
# All fields left in the targets_dict are unrecognized.
816824
return cls(*common_args, targets, delegations, targets_dict)
817825

818826
def to_dict(self) -> Dict[str, Any]:
819827
"""Returns the dict representation of self."""
820828
targets_dict = self._common_fields_to_dict()
821-
targets_dict.update(
822-
{
823-
"targets": self.targets,
824-
"delegations": self.delegations,
825-
}
826-
)
829+
targets_dict["targets"] = self.targets
830+
if self.delegations:
831+
targets_dict["delegations"] = self.delegations
827832
return targets_dict
828833

829834
# Modification.

0 commit comments

Comments
 (0)