fix: write the cut md5 rule once

Six places read an md5 shorter than 32 digits, each with its own bound.
digest_verdict accepted any prefix down to one digit, while Batocera
compares the whole string (batocera-systems:1088). A cut value now names
a dump only when one held md5 starts with it (cut_md5_owner, read by the
scraper, the resolver and --from-md5), and every verdict compares whole
digests.
This commit is contained in:
Abdessamad Derraz committed 2026-11-06 15:18:38 +01:00
1 parent 8af7b0da13
commit ed6643022d
10 files changed
+71 -78

No files matched your search

+15 -14
View File
@@ -1434,6 +1434,15 @@ def resolve_local_file(
names_to_try.append(hint_base)
md5_list = parse_md5_list(file_entry.get("md5"))
files_db = db.get("files", {})
by_md5 = db.get("indexes", {}).get("by_md5", {})
# A value cut short stands for the one held md5 it starts, and only
# then (cut_md5_owner): the dump it names is still name-guarded below.
cut_owners = {
cut: owner for cut in md5_list
if len(cut) < 32 and (owner := cut_md5_owner(cut, by_md5))
}
md5_list = [cut_owners.get(value, value) for value in md5_list]
sha1_candidates = [
str(value).strip().lower()
for value in (sha1 if isinstance(sha1, list) else [sha1] if sha1 else [])
@@ -1461,8 +1470,6 @@ def resolve_local_file(
has_strong_hash = bool(
sha1_candidates or sha256_candidates or md5_list or crc_candidates
)
files_db = db.get("files", {})
by_md5 = db.get("indexes", {}).get("by_md5", {})
by_name = db.get("indexes", {}).get("by_name", {})
by_path_suffix = db.get("indexes", {}).get("by_path_suffix", {})
@@ -1483,7 +1490,7 @@ def resolve_local_file(
# With zipped_file the MD5 identifies the member, not the container.
if md5_list and not zipped_file:
actual_md5 = str(entry.get("md5", "")).lower()
if not any(actual_md5.startswith(expected) for expected in md5_list):
if actual_md5 not in md5_list:
return None
statuses.append("md5_exact")
if crc_candidates:
@@ -1567,20 +1574,13 @@ def resolve_local_file(
sha1_match = by_md5.get(md5_candidate)
if sha1_match and sha1_match in files_db:
path = files_db[sha1_match]["path"]
# Full MD5 (32 chars) is a strong identifier: trust it
# without name guard. Truncated MD5 still needs name check
# to avoid cross-contamination.
# A full md5 as declared is a strong identifier: trusted
# without name guard. One completed from a cut value still
# needs the name check to avoid cross-contamination.
status = _record_match_status(sha1_match)
if status and os.path.exists(path):
if len(md5_candidate) >= 32 or _md5_name_ok(path):
if md5_candidate not in cut_owners.values() or _md5_name_ok(path):
return path, status
if len(md5_candidate) < 32:
for db_md5, db_sha1 in by_md5.items():
if db_md5.startswith(md5_candidate) and db_sha1 in files_db:
path = files_db[db_sha1]["path"]
status = _record_match_status(db_sha1)
if status and os.path.exists(path) and _md5_name_ok(path):
return path, status
# 2b. Path suffix lookup is useful for same-named regional files, but it
# is identity evidence only when no content hash was declared. A stale
@@ -3586,6 +3586,7 @@ from hashing import ( # noqa: E402,F401
md5sum,
md5_composite,
parse_md5_list,
cut_md5_owner,
_ALL_ALGORITHMS,
_md5_composite_cache,
)