fix: a listed hash the other layer holds is agreement

This commit is contained in:
Abdessamad Derraz committed 2026-10-09 21:59:38 +02:00
1 parent 606d91ebab
commit 8944486597
2 files changed
+80 -8

No files matched your search

+54 -8
View File
@@ -242,10 +242,14 @@ def find_conflicts(
continue
by_slot.setdefault(key, []).append(claim)
# A declaration that lists several accepted hashes is satisfied by any of
# them: Recalbox names both scph1001.bin revisions, and mesence names
# four BS-X dumps among which System.dat pins one. The resolver stops on
# the first it finds, so the paths differ while both layers agree.
disputed = {
key: claims
for key, claims in by_slot.items()
if all(c.local_path != by_dest[key].local_path for c in claims)
if not any(_satisfies(c, by_dest[key], db) for c in claims)
}
return [
@@ -455,6 +459,51 @@ def find_collisions(
return collisions
def _satisfies(profile: Claim, platform: Claim, db: dict) -> bool:
"""Whether the two layers agree on what ships at this destination.
They agree when they resolve to the same file, when the platform's own
hash list accepts the profile's file, or when the profile's accepts the
platform's. A contradiction needs both lists to reject the other side.
"""
if profile.local_path == platform.local_path:
return True
if (
profile.local_path
and _lists_hashes(platform.entry)
and _accepted_by_all(profile.local_path, [platform.entry], db)
):
return True
return bool(
platform.local_path
and _lists_hashes(profile.entry)
and _accepted_by_all(platform.local_path, [profile.entry], db)
)
def _lists_hashes(entry: dict) -> bool:
"""Whether a declaration says anything about content at all."""
return any(
_declared_hashes(entry, field_name)
for field_name in ("md5", "sha1", "sha256", "crc32")
)
def _declared_hashes(entry: dict, field_name: str) -> set[str]:
"""Hashes an entry accepts for one field, declared as a string or a list."""
value = entry.get(field_name)
values = value if isinstance(value, list) else [value]
# Recalbox writes its md5 list comma separated, and profiles do the
# same for sha256 (mesence names four BS-X dumps in one string).
return {
part.strip().lower()
for v in values
if v
for part in str(v).split(",")
if part.strip()
}
def _accepted_by_all(path: str, entries: list[dict], db: dict) -> bool:
"""Whether every declaration's own hashes accept the file at path."""
record = next(
@@ -470,13 +519,10 @@ def _accepted_by_all(path: str, entries: list[dict], db: dict) -> bool:
):
return False
continue
md5s = parse_md5_list(entry.get("md5"))
if md5s and str(record.get("md5", "")).lower() not in md5s:
return False
sha1 = entry.get("sha1")
sha1s = [sha1] if isinstance(sha1, str) else list(sha1 or [])
if sha1s and str(record.get("sha1", "")).lower() not in {h.lower() for h in sha1s}:
return False
for field_name in ("md5", "sha1", "sha256", "crc32"):
declared = _declared_hashes(entry, field_name)
if declared and str(record.get(field_name, "")).lower() not in declared:
return False
return True
+26
View File
@@ -157,6 +157,32 @@ class TestConflicts(unittest.TestCase):
conflicts = slots.find_conflicts(self._config("m" * 32), profile, REGIONS_DB)
self.assertEqual(conflicts, [])
def test_a_platform_list_naming_the_profile_file_is_agreement(self):
"""Recalbox names both scph1001.bin revisions; the resolver stops on
the first, the profile pins the other, and --strict reported a
contradiction nobody has to correct."""
conflicts = slots.find_conflicts(
self._config(f"{'m' * 32},{'n' * 32}"),
self._profile("GC/JAP/IPL.bin"),
REGIONS_DB,
)
self.assertEqual(conflicts, [])
def test_a_profile_list_naming_the_shipped_file_is_agreement(self):
"""mesence accepts four BS-X dumps, one of which System.dat pins;
the slot read as contested and the pack swapped the catalogued
dump for an uncatalogued one."""
profile = {
"mesence": {
"files": [
{"name": "IPL.bin", "path": "GC/JAP/IPL.bin",
"md5": [ "n" * 32, "m" * 32 ]},
]
}
}
conflicts = slots.find_conflicts(self._config("m" * 32), profile, REGIONS_DB)
self.assertEqual(conflicts, [])
def test_a_name_only_resolution_asserts_nothing(self):
# No hash and no path that the repository carries: the entry is
# answered by filename alone and cannot contradict anything.