"""Finding the bytes a pack entry names. Resolution proper lives in common; this layer adds what only a build needs: storage tiers, release assets for files too large to commit, and the offline switch that forbids reaching for the network.""" from __future__ import annotations import re _HEX_RE = re.compile(r"\b([0-9a-fA-F]{8,40})\b") from largefiles import fetch_large_file import os from hashing import parse_md5_list from common import resolve_local_file # CLI-wide network policy. Public helpers can override it explicitly, while # every CLI mode inherits --offline (including large-file release fallback). _OFFLINE = False def set_offline(value: bool) -> None: """Forbid every network reach for the rest of the run.""" global _OFFLINE _OFFLINE = value def _detect_hash_type(h: str) -> str: n = len(h) if n == 40: return "sha1" if n == 32: return "md5" if n == 8: return "crc32" return "md5" def parse_hash_input(raw: str) -> list[tuple[str, str]]: """Parse comma-separated hash string into (type, hash) tuples.""" results: list[tuple[str, str]] = [] for part in raw.split(","): part = part.strip().lower() if not part: continue m = _HEX_RE.search(part) if m: h = m.group(1) results.append((_detect_hash_type(h), h)) return results def parse_hash_file(path: str) -> list[tuple[str, str]]: """Parse hash file (one per line, comments with #, mixed formats).""" results: list[tuple[str, str]] = [] with open(path) as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue m = _HEX_RE.search(line.lower()) if m: h = m.group(1) results.append((_detect_hash_type(h), h)) return results def lookup_hashes( hashes: list[tuple[str, str]], db: dict, bios_dir: str, emulators_dir: str, platforms_dir: str, ) -> None: """Print diagnostic info for each hash.""" files_db = db.get("files", {}) by_md5 = db.get("indexes", {}).get("by_md5", {}) by_crc32 = db.get("indexes", {}).get("by_crc32", {}) for hash_type, hash_val in hashes: sha1 = None if hash_type == "sha1" and hash_val in files_db: sha1 = hash_val elif hash_type == "md5": sha1 = by_md5.get(hash_val) elif hash_type == "crc32": sha1 = by_crc32.get(hash_val) if not sha1 or sha1 not in files_db: print(f"\n{hash_type.upper()}: {hash_val}") print(" NOT FOUND in database") continue entry = files_db[sha1] name = entry.get("name", "?") md5 = entry.get("md5", "?") paths = entry.get("paths") or [] aliases = entry.get("aliases") or [] print(f"\n{hash_type.upper()}: {hash_val}") print(f" SHA1: {sha1}") print(f" MD5: {md5}") print(f" Name: {name}") if paths: print(f" Path: {paths[0]}") if aliases: print(f" Aliases: {aliases}") # Check if file exists in repo (by path or by resolve_local_file) in_repo = False if paths: primary = os.path.join(bios_dir, paths[0]) if os.path.exists(primary): in_repo = True if not in_repo: try: fe_check = {"name": name, "sha1": sha1, "md5": md5} local, status = resolve_file(fe_check, db, bios_dir, {}) if local and status != "not_found": in_repo = True except (KeyError, OSError): pass print(f" In repo: {'YES' if in_repo else 'NO'}") def resolve_file( file_entry: dict, db: dict, bios_dir: str, zip_contents: dict | None = None, dest_hint: str = "", data_dir_registry: dict | None = None, *, offline: bool | None = None, ) -> tuple[str | None, str]: """Resolve a BIOS file with storage tiers and release asset fallback. Wraps common.resolve_local_file() with pack-specific logic for storage tiers (external/user_provided), large file release assets, and MAME clone mapping (deduped ZIPs). """ storage = file_entry.get("storage", "embedded") if storage == "user_provided": return None, "user_provided" if storage == "external": return None, "external" path, status = resolve_local_file( file_entry, db, zip_contents, dest_hint=dest_hint, data_dir_registry=data_dir_registry, ) if path and status != "hash_mismatch": return path, status # Large files from GitHub release assets -tried when local file is # missing OR has a hash mismatch (wrong variant on disk) name = file_entry.get("name", "") sha1 = file_entry.get("sha1") first_sha1 = (sha1[0] if sha1 else "") if isinstance(sha1, list) else (sha1 or "") md5_list = parse_md5_list(file_entry.get("md5")) first_md5 = md5_list[0] if md5_list else "" if file_entry.get("unsourceable") and not (first_sha1 or first_md5): # The asset store is addressed by file name: with no hash to check, # it would hand back the same-named file of another game, which is # exactly what the flag forbids. return (path, status) if path else (None, "not_found") cached = fetch_large_file( name, expected_sha1=first_sha1, expected_md5=first_md5, offline=_OFFLINE if offline is None else offline, ) if cached: return cached, "release_asset" # Fall back to hash_mismatch local file if release asset unavailable if path: return path, status return None, "not_found"