mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-12 06:23:23 -05:00
fix: let a deep path answer for the file it names
This commit is contained in:
1 parent
e0c60f2f5a
commit
3c16f6f640
5 files changed
+102451
-543
No files matched your search
+45
-12
@@ -637,9 +637,33 @@ def resolve_local_file(
|
||||
# layout, so the two meet on a tail rather than on the whole string.
|
||||
# Longest tail first, and never down to the bare filename: that is the
|
||||
# weaker step below, and five files answer to GameIndex.yaml.
|
||||
# Two kinds of ambiguity appear once every tail is indexed. A
|
||||
# shortened tail can be claimed by several emulators shipping the same
|
||||
# asset tree, and it then proves nothing: skip it. The full tail can be
|
||||
# claimed twice as well, one storing it at the root of its own
|
||||
# directory and one nested deeper; there the file whose path carries
|
||||
# the least on top of the destination is the one that IS the
|
||||
# destination.
|
||||
# The descent drops at most one leading segment. A destination and the
|
||||
# tree differ by the wrapper directory at most; beyond that the tail
|
||||
# stops describing this file and starts describing whichever emulator
|
||||
# happens to store the same asset tree.
|
||||
hint_parts = dest_hint.split("/")
|
||||
for start in range(len(hint_parts) - 1):
|
||||
for match_sha1 in by_path_suffix.get("/".join(hint_parts[start:]), []):
|
||||
for start in range(min(2, len(hint_parts) - 1)):
|
||||
tail = "/".join(hint_parts[start:])
|
||||
candidates = by_path_suffix.get(tail, [])
|
||||
if start and len(candidates) > 1:
|
||||
continue
|
||||
if len(candidates) > 1:
|
||||
depth = len(tail.split("/"))
|
||||
candidates = sorted(
|
||||
candidates,
|
||||
key=lambda h: len(
|
||||
files_db.get(h, {}).get("path", "").split("/")
|
||||
)
|
||||
- depth,
|
||||
)
|
||||
for match_sha1 in candidates:
|
||||
if match_sha1 not in files_db:
|
||||
continue
|
||||
path = files_db[match_sha1]["path"]
|
||||
@@ -704,15 +728,24 @@ def resolve_local_file(
|
||||
path = entry["path"]
|
||||
if os.path.exists(path) and path not in seen_paths:
|
||||
seen_paths.add(path)
|
||||
candidates.append((path, entry.get("md5", "")))
|
||||
candidates.append(
|
||||
(path, entry.get("md5", ""), entry.get("size"))
|
||||
)
|
||||
|
||||
if zipped_file:
|
||||
candidates = [c for c in candidates if ".zip" in os.path.basename(c[0])]
|
||||
# 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
|
||||
# excludes, because a present file of the wrong size is reported as
|
||||
# present and untested, and dropping it would report it as absent.
|
||||
sized = [c for c in candidates if name_match_size_ok(file_entry, c[2])]
|
||||
if sized:
|
||||
candidates = sized
|
||||
|
||||
if candidates:
|
||||
if zipped_file:
|
||||
candidates = [
|
||||
(p, m) for p, m in candidates if ".zip" in os.path.basename(p)
|
||||
]
|
||||
if md5_set and not (sha1_candidates or sha256_candidates or crc_raw):
|
||||
for path, db_md5 in candidates:
|
||||
for path, db_md5, _size in candidates:
|
||||
if ".zip" in os.path.basename(path):
|
||||
try:
|
||||
composite = md5_composite(path).lower()
|
||||
@@ -725,20 +758,20 @@ def resolve_local_file(
|
||||
# When zipped_file is set, only accept candidates that contain it
|
||||
if zipped_file:
|
||||
valid = []
|
||||
for path, m in candidates:
|
||||
for path, m, size in candidates:
|
||||
try:
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
inner_names = {n.casefold() for n in zf.namelist()}
|
||||
if zipped_file.casefold() in inner_names:
|
||||
valid.append((path, m))
|
||||
valid.append((path, m, size))
|
||||
except (zipfile.BadZipFile, OSError):
|
||||
pass
|
||||
if valid:
|
||||
primary = [p for p, _ in valid if "/.variants/" not in p]
|
||||
primary = [c[0] for c in valid if "/.variants/" not in c[0]]
|
||||
return (primary[0] if primary else valid[0][0]), "hash_mismatch"
|
||||
# No candidate contains the zipped_file -fall through to step 5
|
||||
elif not unsourceable:
|
||||
primary = [p for p, _ in candidates if "/.variants/" not in p]
|
||||
primary = [c[0] for c in candidates if "/.variants/" not in c[0]]
|
||||
return (primary[0] if primary else candidates[0][0]), "hash_mismatch"
|
||||
|
||||
# 5. zipped_file content match via pre-built index (last resort:
|
||||
|
||||
+24
-15
@@ -165,17 +165,24 @@ def scan_bios_dir(bios_dir: Path, cache: dict, force: bool) -> tuple[dict, dict,
|
||||
return files, aliases, new_cache
|
||||
|
||||
|
||||
def _path_suffix(rel_path: str) -> str:
|
||||
"""Extract the path suffix after bios/Manufacturer/Console/.
|
||||
def _path_suffixes(rel_path: str) -> list[str]:
|
||||
"""Every tail of a stored path that can answer a declared destination.
|
||||
|
||||
bios/Nintendo/GameCube/GC/USA/IPL.bin -> GC/USA/IPL.bin
|
||||
bios/Sony/PlayStation/scph5501.bin -> scph5501.bin
|
||||
A destination and the tree meet on a tail, and which tail is unknowable
|
||||
from here: one profile writes GC/USA/IPL.bin, another writes the whole
|
||||
nand/.../00000000.app.romfs. Indexing only the tail after
|
||||
bios/Manufacturer/Console answered the first and never the second, so
|
||||
every same-named file collapsed onto one entry.
|
||||
|
||||
The bare filename is left out on purpose. by_name already holds it, and a
|
||||
name alone is the weakest evidence there is.
|
||||
|
||||
bios/Nintendo/GameCube/GC/USA/IPL.bin -> GC/USA/IPL.bin, USA/IPL.bin
|
||||
"""
|
||||
parts = rel_path.replace("\\", "/").split("/")
|
||||
# Skip: bios / Manufacturer / Console (3 segments)
|
||||
if len(parts) > 3 and parts[0] == "bios":
|
||||
return "/".join(parts[3:])
|
||||
return parts[-1]
|
||||
parts = [p for p in rel_path.replace("\\", "/").split("/") if p]
|
||||
if parts and parts[0] == "bios":
|
||||
parts = parts[1:]
|
||||
return ["/".join(parts[-depth:]) for depth in range(len(parts), 1, -1)]
|
||||
|
||||
|
||||
def build_indexes(files: dict, aliases: dict) -> dict:
|
||||
@@ -198,12 +205,13 @@ def build_indexes(files: dict, aliases: dict) -> dict:
|
||||
by_sha256[entry["sha256"]] = sha1
|
||||
|
||||
# Path suffix index for regional variant resolution
|
||||
suffix = _path_suffix(entry["path"])
|
||||
if suffix != name:
|
||||
# Only index when suffix adds info beyond the filename
|
||||
for suffix in _path_suffixes(entry["path"]):
|
||||
if suffix == name:
|
||||
continue
|
||||
if suffix not in by_path_suffix:
|
||||
by_path_suffix[suffix] = []
|
||||
by_path_suffix[suffix].append(sha1)
|
||||
if sha1 not in by_path_suffix[suffix]:
|
||||
by_path_suffix[suffix].append(sha1)
|
||||
|
||||
# Add alias names to by_name index (aliases have different filenames for same SHA1)
|
||||
for sha1, alias_list in aliases.items():
|
||||
@@ -214,8 +222,9 @@ def build_indexes(files: dict, aliases: dict) -> dict:
|
||||
if sha1 not in by_name[name]:
|
||||
by_name[name].append(sha1)
|
||||
# Also index alias paths in by_path_suffix
|
||||
suffix = _path_suffix(alias["path"])
|
||||
if suffix != name:
|
||||
for suffix in _path_suffixes(alias["path"]):
|
||||
if suffix == name:
|
||||
continue
|
||||
if suffix not in by_path_suffix:
|
||||
by_path_suffix[suffix] = []
|
||||
if sha1 not in by_path_suffix[suffix]:
|
||||
|
||||
Reference in new issue
Block a user