Skip to content

Commit b4a6c7e

Browse files
committed
Take order into account for certain cases
After we have dropped OrderedDict in e3b267e we are relying on python3.7+ default behavior to preserve the insertion order, but there is one caveat. When comparing dictionaries the order is still irrelevant compared to OrderedDict. For example: >>> OrderedDict([(1,1), (2,2)]) == OrderedDict([(2,2), (1,1)]) False >>> dict([(1,1), (2,2)]) == dict([(2,2), (1,1)]) True There are two special attributes, defined in the specification, where the order makes a difference when comparing two objects: - Metadata.signatures - Targets.delegations.roles. We want to make sure that the order in those two cases makes a difference when comparing two objects and that's why those changes are required inside two __eq__ implementations. Signed-off-by: Martin Vrachev <[email protected]>
1 parent 686d227 commit b4a6c7e

File tree

2 files changed

+99
-1
lines changed

2 files changed

+99
-1
lines changed

tests/test_metadata_eq_.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
import os
1111
import sys
1212
import unittest
13-
from typing import Any, ClassVar, Dict
13+
from typing import Any, ClassVar, Dict, List
14+
15+
from securesystemslib.signer import SSlibSigner
1416

1517
from tests import utils
18+
from tests.repository_simulator import RepositorySimulator
1619
from tuf.api.metadata import (
1720
TOP_LEVEL_ROLE_NAMES,
1821
DelegatedRole,
@@ -60,6 +63,35 @@ def test_metadata_eq_(self, test_case_data: Any) -> None:
6063
setattr(md_2, self.case_name, test_case_data)
6164
self.assertNotEqual(md, md_2)
6265

66+
def test_md_eq_signatures_reversed_order(self) -> None:
67+
# Test comparing objects with same signatures but different order.
68+
69+
# Remove all signatures and create new ones.
70+
md = Metadata.from_bytes(self.metadata["snapshot"])
71+
md.signatures = {}
72+
md_2 = copy.deepcopy(md)
73+
signatures: List[SSlibSigner] = []
74+
# Assign the new signatures to the md object in the straight order.
75+
for i in ["0", "1", "2"]:
76+
_, signer = RepositorySimulator.create_key()
77+
signature = signer.sign(md.to_bytes())
78+
signatures.append(signature)
79+
md.signatures[i] = signature
80+
81+
# Assign the new signatures in a reverse order for the md_2 object.
82+
for j in [2, 1, 0]:
83+
md_2.signatures[str(j)] = signatures[j]
84+
85+
# Assert that both objects are not the same because of signatures order.
86+
self.assertNotEqual(md, md_2)
87+
88+
# but if we fix the signatures order they will be equal
89+
md_2.signatures = {}
90+
for j in [0, 1, 2]:
91+
md_2.signatures[str(j)] = signatures[j]
92+
93+
self.assertEqual(md, md_2)
94+
6395
def test_md_eq_special_signatures_tests(self) -> None:
6496
# Test that metadata objects with different signatures are not equal.
6597
md = Metadata.from_bytes(self.metadata["snapshot"])
@@ -284,6 +316,54 @@ def test_delegations_eq_(self, test_case_data: Any) -> None:
284316
setattr(delegations_2, self.case_name, test_case_data)
285317
self.assertNotEqual(delegations, delegations_2)
286318

319+
def test_delegations_eq_roles_reversed_order(self) -> None:
320+
# Test comparing objects with same delegated roles but different order.
321+
role_one_dict = {
322+
"keyids": ["keyid1"],
323+
"name": "a",
324+
"terminating": False,
325+
"paths": ["fn1"],
326+
"threshold": 1,
327+
}
328+
role_two_dict = {
329+
"keyids": ["keyid2"],
330+
"name": "b",
331+
"terminating": True,
332+
"paths": ["fn2"],
333+
"threshold": 4,
334+
}
335+
role_one = DelegatedRole.from_dict(role_one_dict)
336+
role_two = DelegatedRole.from_dict(role_two_dict)
337+
338+
delegations_dict = {
339+
"keys": {
340+
"keyid2": {
341+
"keytype": "ed25519",
342+
"scheme": "ed25519",
343+
"keyval": {"public": "bar"},
344+
}
345+
},
346+
"roles": [],
347+
}
348+
delegations = Delegations.from_dict(copy.deepcopy(delegations_dict))
349+
delegations.roles["a"] = role_one
350+
delegations.roles["b"] = role_two
351+
352+
# Create a second delegations obj with reversed roles order
353+
delegations_2 = Delegations.from_dict(delegations_dict)
354+
delegations_2.roles["b"] = copy.deepcopy(role_two)
355+
delegations_2.roles["a"] = copy.deepcopy(role_one)
356+
357+
# Both objects are not the equal because of delegated roles order.
358+
self.assertNotEqual(delegations, delegations_2)
359+
360+
# but if we fix the delegated roles order they will be equal
361+
delegations_2.roles = {}
362+
delegations_2.roles["a"] = copy.deepcopy(role_one)
363+
delegations_2.roles["b"] = copy.deepcopy(role_two)
364+
365+
self.assertEqual(delegations, delegations_2)
366+
287367
# Common attributes between TargetFile and MetaFile doesn't need testing.
288368
targetfile_attributes_modifications: utils.DataSet = {
289369
"path": "",

tuf/api/metadata.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ def __eq__(self, other: Any) -> bool:
131131
if not isinstance(other, Metadata):
132132
return False
133133

134+
# Check that the order of the signatures is the same as dictionaries
135+
# are insensitive to order in comparisons (see issue #1788).
136+
if (
137+
isinstance(self.signatures, dict)
138+
and isinstance(other.signatures, dict)
139+
and list(self.signatures.keys()) != list(other.signatures.keys())
140+
):
141+
return False
142+
134143
return (
135144
self.signatures == other.signatures
136145
and self.signed == other.signed
@@ -1428,6 +1437,15 @@ def __eq__(self, other: Any) -> bool:
14281437
if not isinstance(other, Delegations):
14291438
return False
14301439

1440+
# Check that the order of the roles is the same as dictionaries
1441+
# are insensitive to order in comparisons (see issue #1788).
1442+
if (
1443+
isinstance(self.roles, dict)
1444+
and isinstance(other.roles, dict)
1445+
and list(self.roles.keys()) != list(other.roles.keys())
1446+
):
1447+
return False
1448+
14311449
return (
14321450
self.keys == other.keys
14331451
and self.roles == other.roles

0 commit comments

Comments
 (0)