fix: narrow hashless name matches to their maker

AmberELEC's nec-pc-88 declares DISK.ROM with an empty md5. QUASI88 reads
that name as n88sub.rom, the 2 KB PC-88 disk ROM (libretro.c:1085), but
the name step served the 16 KB MSX disk ROM, the only primary of that
name. The quasi88 alias never reached by_name, which refuses an alias a
primary already carries.

load_platform_config now writes each entry's system as platform_system.
A platform entry with no hash and no owning emulator, whose system id
names a maker, takes no name candidate filed under another maker, in
the name step and in the hash_mismatch step after it. When nothing is
left, by_shadowed_alias, the profile aliases by_name refuses, answers
within the system's maker folder. names_manufacturer and BUCKET_FOLDERS
move to common.py, and generate_site imports them.

Across every registered platform the only resolution that moves is
AmberELEC nec-pc-88 DISK.ROM, from bios/Microsoft/MSX/DISK.ROM to
bios/NEC/PC-88/N88SUB.ROM. Manifests, truth and exports are otherwise
byte-identical.
This commit is contained in:
Abdessamad Derraz committed 2026-11-05 23:17:04 +01:00
1 parent 9a66ac3219
commit d1ebd411ab
11 files changed
+578 -27

No files matched your search

+104
View File
@@ -90,6 +90,71 @@ def _casefold_name_index(by_name: dict) -> dict[str, list[str]]:
return folded
# bios/Other/<emulator> and bios/Arcade/<emulator or board> file a copy by
# who loads it, not by machine.
BUCKET_FOLDERS = frozenset({"Other", "Arcade"})
def names_manufacturer(sys_id: str, manufacturer: str) -> bool:
"""Whether a system id names a maker folder: nec-pc-88 names NEC."""
slug = manufacturer.lower().replace(" ", "-")
return sys_id == slug or sys_id.startswith(f"{slug}-")
def _maker_folder(path: str, bios_dir: str) -> str | None:
"""The bios/<maker>/ folder a collection path is filed under."""
prefix = f"{bios_dir.rstrip('/')}/"
if not path.startswith(prefix):
return None
parts = path[len(prefix):].split("/")
return parts[0] if len(parts) >= 2 else None
# One slot, keyed like the casefold view: the files dict, its size, the folders.
_maker_folders_cache: list[tuple[dict, int, frozenset[str]]] = []
def _maker_folders(db: dict) -> frozenset[str]:
"""Every maker folder the collection files a copy under."""
files_db = db.get("files", {})
if _maker_folders_cache:
source, size, folders = _maker_folders_cache[0]
if source is files_db and size == len(files_db):
return folders
bios_dir = str(db.get("bios_dir") or "bios")
folders = frozenset(
folder
for entry in files_db.values()
if (folder := _maker_folder(str(entry.get("path", "")), bios_dir))
and folder not in BUCKET_FOLDERS
)
_maker_folders_cache[:] = [(files_db, len(files_db), folders)]
return folders
def system_makers(file_entry: dict, db: dict) -> frozenset[str]:
"""The maker folders a platform entry's system names.
Only a platform's own entry is asked: one that names an owning emulator
is placed by that emulator's tree. Empty when the system names no maker
(pc88, arcade), so nothing is narrowed there.
"""
sys_id = str(file_entry.get("platform_system") or "")
if not sys_id or _owner_tokens(file_entry):
return frozenset()
return frozenset(
folder for folder in _maker_folders(db) if names_manufacturer(sys_id, folder)
)
def other_maker(path: str, makers: frozenset[str], db: dict) -> bool:
"""Whether a path is filed under a maker the entry's system does not name."""
if not makers:
return False
folder = _maker_folder(path, str(db.get("bios_dir") or "bios"))
return folder is not None and folder not in BUCKET_FOLDERS and folder not in makers
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.
@@ -357,6 +422,19 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -
for sys_id, system in config.get("systems", {}).items()
}
# Each entry carries the system that declares it: a hashless name match is
# narrowed to the maker folder that system names (resolve_local_file).
# Fresh dicts, since a YAML anchor can share one entry between systems.
config["systems"] = {
sys_id: {
**system,
"files": [{**f, "platform_system": sys_id} for f in system["files"]],
}
if isinstance(system, dict) and system.get("files")
else system
for sys_id, system in config.get("systems", {}).items()
}
# The registry key, which per-platform profile fields (installed_on,
# not_bundled_on) name: the display name differs (MiSTer FPGA).
config["_key"] = platform_name
@@ -1573,6 +1651,10 @@ def resolve_local_file(
# hash and path steps above run first, so collecting the real bytes makes
# the entry resolve.
unsourceable = bool(file_entry.get("unsourceable")) or own_file_absent
# A platform's own hashless entry whose system names a maker takes no copy
# filed under another maker: AmberELEC's nec-pc-88 DISK.ROM, which QUASI88
# reads as the 2 KB PC-88 disk ROM, got the 16 KB MSX one by name.
makers = frozenset() if has_strong_hash else system_makers(file_entry, db)
if not has_strong_hash and not unsourceable:
candidates = []
@@ -1637,6 +1719,26 @@ def resolve_local_file(
continue
if os.path.exists(path) and path not in candidates:
candidates.append(path)
if makers and candidates:
candidates = [p for p in candidates if not other_maker(p, makers, db)]
if not candidates:
# The name belongs to another maker's file here. An emulator
# of this system may still read it as an alias of one of its
# own files, an alias by_name leaves out because a primary
# already carries the name: QUASI88 reads DISK.ROM as
# n88sub.rom.
shadowed = db.get("indexes", {}).get("by_shadowed_alias", {})
for try_name in own_names:
for match_sha1 in shadowed.get(try_name, []):
path = files_db.get(match_sha1, {}).get("path", "")
if (
_maker_folder(path, str(db.get("bios_dir") or "bios"))
in makers
and _size_ok(match_sha1)
and os.path.exists(path)
and path not in candidates
):
candidates.append(path)
if candidates:
if zipped_file:
candidates = [p for p in candidates if ".zip" in os.path.basename(p)]
@@ -1701,6 +1803,8 @@ def resolve_local_file(
if zipped_file:
candidates = [c for c in candidates if ".zip" in os.path.basename(c[0])]
# Another maker's file is no mismatch of this one, it is another file.
candidates = [c for c in candidates if not other_maker(c[0], makers, db)]
# A size the emulator itself verifies settles a name collision, so a
# candidate that fits is preferred over one that does not: "ROM" answers
# both an Apple IIgs image and a Macintosh one. It ranks rather than