Skip to content

Commit 3419dda

Browse files
committed
Configure unrecognized_fields per instance
It's important to make unrecognized_fields an instance level variable instead of class-level variable, because two root files for example could have a different set of unrecognized fields. Additional changes were necessary to make it work. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
1 parent 435f638 commit 3419dda

File tree

2 files changed

+66
-31
lines changed

2 files changed

+66
-31
lines changed

tests/test_api.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,28 +330,37 @@ def test_metadata_targets(self):
330330
# Verify that data is updated
331331
self.assertEqual(targets.signed.targets[filename], fileinfo)
332332

333-
def setup_json_dict_with_unrecognized_field(self, file_path):
333+
def setup_dict_with_unrecognized_field(self, file_path, field, value):
334334
json_dict = {}
335335
with open(file_path) as f:
336336
json_dict = json.loads(f.read())
337337
# We are changing the json dict without changing the signature.
338338
# This could be a problem if we want to do verification on this dict.
339-
json_dict["signed"]["foo"] = "bar"
339+
json_dict["signed"][field] = value
340340
return json_dict
341341

342342
def test_support_for_unrecognized_fields(self):
343343
for metadata in ["root", "timestamp", "snapshot", "targets"]:
344344
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")
345-
json_dict = self.setup_json_dict_with_unrecognized_field(path)
345+
dict1 = self.setup_dict_with_unrecognized_field(path, "f", "b")
346346
# Test that the metadata classes store unrecognized fields when
347347
# initializing and passes them when casting the instance to a dict.
348348

349349
# TODO: Remove the deepcopy when Metadata.from_dict() doesn't have
350350
# the side effect to destroy the passed dictionary.
351-
temp_copy = copy.deepcopy(json_dict)
351+
temp_copy = copy.deepcopy(dict1)
352352
metadata_obj = Metadata.from_dict(temp_copy)
353353

354-
self.assertEqual(json_dict["signed"], metadata_obj.signed.to_dict())
354+
self.assertEqual(dict1["signed"], metadata_obj.signed.to_dict())
355+
356+
# Test that two instances of the same class could have different
357+
# unrecognized fields.
358+
dict2 = self.setup_dict_with_unrecognized_field(path, "f2", "b2")
359+
temp_copy2 = copy.deepcopy(dict2)
360+
metadata_obj2 = Metadata.from_dict(temp_copy2)
361+
self.assertNotEqual(
362+
metadata_obj.signed.to_dict(), metadata_obj2.signed.to_dict()
363+
)
355364

356365

357366
# Run unit test.

tuf/api/metadata.py

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -300,18 +300,24 @@ class Signed:
300300
spec_version: The TUF specification version number (semver) the
301301
metadata format adheres to.
302302
expires: The metadata expiration datetime object.
303-
known_fields: A dictionary containing the known fields(attributes). Used
304-
when filtering the known fields from all fields in order to retrieve
305-
and store the unrecognized fields. See ADR 0008 for context.
303+
unrecognized_fields: An optional dictionary storing all unrecognized
304+
fields. Used for backward compatibility.
306305
307306
"""
308307

308+
# This list is used when filtering the known fields from all fields
309+
# in order to retrieve and store the unrecognized fields.
309310
known_fields = ["_type", "spec_version", "expires", "version"]
310311
# NOTE: Signed is a stupid name, because this might not be signed yet, but
311312
# we keep it to match spec terminology (I often refer to this as "payload",
312313
# or "inner metadata")
313314
def __init__(
314-
self, _type: str, version: int, spec_version: str, expires: datetime
315+
self,
316+
_type: str,
317+
version: int,
318+
spec_version: str,
319+
expires: datetime,
320+
unrecognized_fields: Optional[Mapping[str, Any]] = {},
315321
) -> None:
316322

317323
self._type = _type
@@ -322,19 +328,29 @@ def __init__(
322328
if version < 0:
323329
raise ValueError(f"version must be >= 0, got {version}")
324330
self.version = version
331+
self.unrecognized_fields = unrecognized_fields
325332

326333
@classmethod
327-
def _store_unrecognized_fields(
334+
def _get_unrecognized_fields(
328335
cls, all_fields: Mapping[str, Any]
329336
) -> Mapping[str, Any]:
330337
"""Utility function filtering the known fields from all in order to
331-
retrieve and store the unrecognized fields."""
338+
get the unrecognized fields."""
339+
340+
_type = all_fields["_type"]
341+
if _type == "root":
342+
cls.known_fields += ["consistent_snapshot", "keys", "roles"]
343+
elif _type == "snapshot" or _type == "timestamp":
344+
cls.known_fields += ["meta"]
345+
elif _type == "targets":
346+
cls.known_fields += ["targets", "delegations"]
347+
332348
unrecognized_fields = {}
333349
for key, value in all_fields.items():
334350
if key not in cls.known_fields:
335351
unrecognized_fields[key] = value
336352

337-
cls.unrecognized_fields = unrecognized_fields
353+
return unrecognized_fields
338354

339355
@staticmethod
340356
def _common_fields_from_dict(signed_dict: Mapping[str, Any]) -> list:
@@ -366,6 +382,7 @@ def _common_fields_to_dict(self) -> Dict[str, Any]:
366382
"version": self.version,
367383
"spec_version": self.spec_version,
368384
"expires": self.expires.isoformat() + "Z",
385+
**self.unrecognized_fields,
369386
}
370387

371388
# Modification.
@@ -426,8 +443,11 @@ def __init__(
426443
consistent_snapshot: bool,
427444
keys: Mapping[str, Any],
428445
roles: Mapping[str, Any],
446+
unrecognized_fields: Optional[Mapping[str, Any]] = {},
429447
) -> None:
430-
super().__init__(_type, version, spec_version, expires)
448+
super().__init__(
449+
_type, version, spec_version, expires, unrecognized_fields
450+
)
431451
# TODO: Add classes for keys and roles
432452
self.consistent_snapshot = consistent_snapshot
433453
self.keys = keys
@@ -436,13 +456,14 @@ def __init__(
436456
@classmethod
437457
def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
438458
"""Creates Root object from its dict representation. """
459+
unrecognized_fields = cls._get_unrecognized_fields(root_dict)
439460
common_args = cls._common_fields_from_dict(root_dict)
440461
consistent_snapshot = root_dict.pop("consistent_snapshot")
441462
keys = root_dict.pop("keys")
442463
roles = root_dict.pop("roles")
443-
cls.known_fields += ["consistent_snapshot", "keys", "roles"]
444-
cls._store_unrecognized_fields(root_dict)
445-
return cls(*common_args, consistent_snapshot, keys, roles)
464+
return cls(
465+
*common_args, consistent_snapshot, keys, roles, unrecognized_fields
466+
)
446467

447468
def to_dict(self) -> Dict[str, Any]:
448469
"""Returns the dict representation of self. """
@@ -452,7 +473,6 @@ def to_dict(self) -> Dict[str, Any]:
452473
"consistent_snapshot": self.consistent_snapshot,
453474
"keys": self.keys,
454475
"roles": self.roles,
455-
**self.unrecognized_fields,
456476
}
457477
)
458478
return root_dict
@@ -505,24 +525,26 @@ def __init__(
505525
spec_version: str,
506526
expires: datetime,
507527
meta: Mapping[str, Any],
528+
unrecognized_fields: Optional[Mapping[str, Any]] = {},
508529
) -> None:
509-
super().__init__(_type, version, spec_version, expires)
530+
super().__init__(
531+
_type, version, spec_version, expires, unrecognized_fields
532+
)
510533
# TODO: Add class for meta
511534
self.meta = meta
512535

513536
@classmethod
514537
def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> "Timestamp":
515538
"""Creates Timestamp object from its dict representation. """
539+
unrecognized_fields = cls._get_unrecognized_fields(timestamp_dict)
516540
common_args = cls._common_fields_from_dict(timestamp_dict)
517541
meta = timestamp_dict.pop("meta")
518-
cls.known_fields += ["meta"]
519-
cls._store_unrecognized_fields(timestamp_dict)
520-
return cls(*common_args, meta)
542+
return cls(*common_args, meta, unrecognized_fields)
521543

522544
def to_dict(self) -> Dict[str, Any]:
523545
"""Returns the dict representation of self. """
524546
timestamp_dict = self._common_fields_to_dict()
525-
timestamp_dict.update({"meta": self.meta, **self.unrecognized_fields})
547+
timestamp_dict.update({"meta": self.meta})
526548
return timestamp_dict
527549

528550
# Modification.
@@ -571,24 +593,27 @@ def __init__(
571593
spec_version: str,
572594
expires: datetime,
573595
meta: Mapping[str, Any],
596+
unrecognized_fields: Optional[Mapping[str, Any]] = {},
574597
) -> None:
575-
super().__init__(_type, version, spec_version, expires)
598+
super().__init__(
599+
_type, version, spec_version, expires, unrecognized_fields
600+
)
576601
# TODO: Add class for meta
577602
self.meta = meta
578603

579604
@classmethod
580605
def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> "Snapshot":
581606
"""Creates Snapshot object from its dict representation. """
607+
unrecognized_fields = cls._get_unrecognized_fields(snapshot_dict)
582608
common_args = cls._common_fields_from_dict(snapshot_dict)
583609
meta = snapshot_dict.pop("meta")
584610
cls.known_fields += ["meta"]
585-
cls._store_unrecognized_fields(snapshot_dict)
586-
return cls(*common_args, meta)
611+
return cls(*common_args, meta, unrecognized_fields)
587612

588613
def to_dict(self) -> Dict[str, Any]:
589614
"""Returns the dict representation of self. """
590615
snapshot_dict = self._common_fields_to_dict()
591-
snapshot_dict.update({"meta": self.meta, **self.unrecognized_fields})
616+
snapshot_dict.update({"meta": self.meta})
592617
return snapshot_dict
593618

594619
# Modification.
@@ -676,21 +701,23 @@ def __init__(
676701
expires: datetime,
677702
targets: Mapping[str, Any],
678703
delegations: Mapping[str, Any],
704+
unrecognized_fields: Optional[Mapping[str, Any]] = {},
679705
) -> None:
680-
super().__init__(_type, version, spec_version, expires)
706+
super().__init__(
707+
_type, version, spec_version, expires, unrecognized_fields
708+
)
681709
# TODO: Add class for meta
682710
self.targets = targets
683711
self.delegations = delegations
684712

685713
@classmethod
686714
def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
687715
"""Creates Targets object from its dict representation. """
716+
unrecognized_fields = cls._get_unrecognized_fields(targets_dict)
688717
common_args = cls._common_fields_from_dict(targets_dict)
689718
targets = targets_dict.pop("targets")
690719
delegations = targets_dict.pop("delegations")
691-
cls.known_fields += ["meta", "targets", "delegations"]
692-
cls._store_unrecognized_fields(targets_dict)
693-
return cls(*common_args, targets, delegations)
720+
return cls(*common_args, targets, delegations, unrecognized_fields)
694721

695722
def to_dict(self) -> Dict[str, Any]:
696723
"""Returns the dict representation of self. """
@@ -699,7 +726,6 @@ def to_dict(self) -> Dict[str, Any]:
699726
{
700727
"targets": self.targets,
701728
"delegations": self.delegations,
702-
**self.unrecognized_fields,
703729
}
704730
)
705731
return targets_dict

0 commit comments

Comments
 (0)