diff --git a/scripts/common.py b/scripts/common.py index 9db5d4f3..18778d5b 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -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) diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 49c79a1c..853abd96 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -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: