mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 21:43:23 -05:00
fix: reject name hits with contradicting size
This commit is contained in:
1 parent
3a4f8a5111
commit
1b9acff2ad
2 files changed
+59
-8
No files matched your search
+31
-2
@@ -116,6 +116,30 @@ def _casefold_name_index(by_name: dict) -> dict[str, list[str]]:
|
||||
return folded
|
||||
|
||||
|
||||
def name_match_size_ok(file_entry: dict, candidate_size: int | None) -> bool:
|
||||
"""Whether a candidate found by name may be the file the entry describes.
|
||||
|
||||
Generic filenames collide across systems: rom1.bin is a Sony PlayStation 2
|
||||
firmware and a Roland SC-55mk2 program ROM. A name carries no evidence on
|
||||
its own, so a size the emulator verifies settles it. A size declared
|
||||
without ``validation: [size]`` is informative and rejects nothing.
|
||||
"""
|
||||
validation = file_entry.get("validation")
|
||||
if isinstance(validation, dict):
|
||||
validation = validation.get("core", [])
|
||||
if "size" not in (validation or []) or candidate_size is None:
|
||||
return True
|
||||
declared = file_entry.get("size")
|
||||
if declared is not None:
|
||||
allowed = declared if isinstance(declared, list) else [declared]
|
||||
return candidate_size in allowed
|
||||
low = file_entry.get("min_size")
|
||||
high = file_entry.get("max_size")
|
||||
if low is not None and candidate_size < low:
|
||||
return False
|
||||
return not (high is not None and candidate_size > high)
|
||||
|
||||
|
||||
def md5_composite(filepath: str | Path) -> str:
|
||||
"""Compute composite MD5 of a ZIP - matches Recalbox's Zip::Md5Composite().
|
||||
|
||||
@@ -521,11 +545,16 @@ def resolve_local_file(
|
||||
return path, "md5_exact"
|
||||
|
||||
# 3. No MD5 = any file with that name or alias (existence check)
|
||||
def _size_ok(match_sha1: str) -> bool:
|
||||
return name_match_size_ok(
|
||||
file_entry, files_db.get(match_sha1, {}).get("size")
|
||||
)
|
||||
|
||||
if not md5_list:
|
||||
candidates = []
|
||||
for try_name in names_to_try:
|
||||
for match_sha1 in by_name.get(try_name, []):
|
||||
if match_sha1 in files_db:
|
||||
if match_sha1 in files_db and _size_ok(match_sha1):
|
||||
path = files_db[match_sha1]["path"]
|
||||
if os.path.exists(path) and path not in candidates:
|
||||
candidates.append(path)
|
||||
@@ -537,7 +566,7 @@ def resolve_local_file(
|
||||
folded = _casefold_name_index(by_name)
|
||||
for try_name in names_to_try:
|
||||
for match_sha1 in folded.get(try_name.casefold(), []):
|
||||
if match_sha1 in files_db:
|
||||
if match_sha1 in files_db and _size_ok(match_sha1):
|
||||
path = files_db[match_sha1]["path"]
|
||||
if os.path.exists(path) and path not in candidates:
|
||||
candidates.append(path)
|
||||
|
||||
@@ -24,6 +24,7 @@ from common import (
|
||||
load_database,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
name_match_size_ok,
|
||||
require_yaml,
|
||||
)
|
||||
|
||||
@@ -113,24 +114,44 @@ def _resolve_source(
|
||||
by_name_lower: dict[str, str],
|
||||
data_names: set[str] | None = None,
|
||||
by_path_suffix: dict | None = None,
|
||||
file_entry: dict | None = None,
|
||||
db_files: dict | None = None,
|
||||
) -> str | None:
|
||||
"""Return the source category for a file, or None if not found.
|
||||
|
||||
Returns ``"bios"`` (in database.json / bios/), ``"data"`` (in data/),
|
||||
or ``None`` (not available anywhere).
|
||||
|
||||
A name hit whose size contradicts a size the emulator verifies is not the
|
||||
file, so it does not count as available.
|
||||
"""
|
||||
|
||||
def _name_hit(name: str) -> bool:
|
||||
"""Whether the files indexed under *name* can be this entry."""
|
||||
if not file_entry or db_files is None:
|
||||
return True
|
||||
sha1s = by_name.get(name) or []
|
||||
if not isinstance(sha1s, list):
|
||||
sha1s = [sha1s]
|
||||
return any(
|
||||
name_match_size_ok(file_entry, db_files.get(sha1, {}).get("size"))
|
||||
for sha1 in sha1s
|
||||
)
|
||||
|
||||
# bios/ via database.json by_name
|
||||
if fname in by_name:
|
||||
if fname in by_name and _name_hit(fname):
|
||||
return "bios"
|
||||
stripped = fname.rstrip("/")
|
||||
basename = stripped.rsplit("/", 1)[-1] if "/" in stripped else None
|
||||
if basename and basename in by_name:
|
||||
if basename and basename in by_name and _name_hit(basename):
|
||||
return "bios"
|
||||
key = fname.lower()
|
||||
if key in by_name_lower:
|
||||
if key in by_name_lower and _name_hit(by_name_lower[key]):
|
||||
return "bios"
|
||||
if basename:
|
||||
if basename.lower() in by_name_lower:
|
||||
if basename.lower() in by_name_lower and _name_hit(
|
||||
by_name_lower[basename.lower()]
|
||||
):
|
||||
return "bios"
|
||||
# bios/ via by_path_suffix (regional variants)
|
||||
if by_path_suffix and fname in by_path_suffix:
|
||||
@@ -300,14 +321,15 @@ def cross_reference(
|
||||
source = "large_file"
|
||||
else:
|
||||
source = _resolve_source(
|
||||
fname, by_name, by_name_lower, data_names, by_path_suffix
|
||||
fname, by_name, by_name_lower, data_names, by_path_suffix,
|
||||
f, db_files,
|
||||
)
|
||||
if source is None:
|
||||
path_field = f.get("path", "")
|
||||
if path_field and path_field != fname:
|
||||
source = _resolve_source(
|
||||
path_field, by_name, by_name_lower,
|
||||
data_names, by_path_suffix,
|
||||
data_names, by_path_suffix, f, db_files,
|
||||
)
|
||||
# Try MD5 hash match
|
||||
if source is None:
|
||||
|
||||
Reference in new issue
Block a user