diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 069debf6..929bb348 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -1349,6 +1349,8 @@ def generate_emulator_pack( total_files = 0 missing_files = [] + rejected_files: list[str] = [] + discrepancies: list[str] = [] seen_destinations: set[str] = set() seen_lower: set[str] = set() seen_parents: set[str] = ( @@ -1515,15 +1517,27 @@ def generate_emulator_pack( # The file verify --emulator credits is the one shipped: a # dump the core's own check rejects gives way to a held one - # it accepts (azahar's otp.bin, dolphin's dsp_rom.bin). - if check_file_validation( + # it accepts (azahar's otp.bin, dolphin's dsp_rom.bin). With + # none, the core would refuse what the pack ships: it is named + # and left out, as verify reports it. + refused = check_file_validation( local_path, fe["name"], validation_index, bios_dir - ): + ) + if refused: better = find_validated_variant( fe, db, local_path, validation_index, bios_dir ) - if better: - local_path = better + if not better: + reason, _owners = refused + rejected_files.append(f"{fe['name']} -{reason}") + continue + local_path = better + elif status == "hash_mismatch": + # Not a check the core runs, so the file still serves; + # it is not the dump the profile documents, and says so. + discrepancies.append( + f"{fe['name']} -not the dump the {emu_name} profile declares" + ) # SHA1 dedup: skip if same physical file AND same destination # (but allow same file to be packed under different destinations, @@ -1545,7 +1559,7 @@ def generate_emulator_pack( total_files += 1 # Remove empty ZIP (no files packed and no missing = nothing to ship) - if total_files == 0 and not missing_files: + if total_files == 0 and not missing_files and not rejected_files: os.unlink(zip_path) # Report @@ -1555,15 +1569,21 @@ def generate_emulator_pack( parts = [f"{ok_count} files packed"] if missing_count: parts.append(f"{missing_count} missing") + if rejected_files: + parts.append(f"{len(rejected_files)} refused by the core") print(f" {zip_path}: {', '.join(parts)}") for name in missing_files: print(f" MISSING: {name}") + for line in rejected_files: + print(f" REFUSED: {line}") + for line in discrepancies: + print(f" DISCREPANCY: {line}") for ref in sorted(set(data_dir_notices)): print( f" Note: data directory '{ref}' required but not included (use refresh_data_dirs.py)" ) - return zip_path if total_files > 0 or missing_files else None + return zip_path if total_files > 0 or missing_files or rejected_files else None def generate_system_pack( diff --git a/scripts/verify.py b/scripts/verify.py index 7ca2f235..71dba10c 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -287,6 +287,19 @@ def verify_entry_sha1( # Severity mapping per platform +def _emulator_severity(status: str, required: bool, hle_fallback: bool) -> str: + """Severity when the emulator's own code is the check. + + A file present but refused by the core's validation, or a different dump + than the one the profile declares, is not a file the core runs with: + read in existence mode it counted OK, and `verify --emulator` announced + 1/1 OK above the line that said the core would refuse it. + """ + if status == Status.UNTESTED: + return Severity.WARNING + return compute_severity(status, required, "existence", hle_fallback) + + def compute_severity( status: str, required: bool, @@ -1499,7 +1512,7 @@ def verify_emulator( prev, 0 ): file_status[dest] = cur - sev = compute_severity(cur, required, "existence", False) + sev = _emulator_severity(cur, required, False) prev_sev = file_severity.get(dest) if prev_sev is None or _SEVERITY_ORDER.get( sev, 0 @@ -1612,7 +1625,7 @@ def verify_emulator( prev, 0 ): file_status[dest] = cur - sev = compute_severity(cur, required, "existence", hle) + sev = _emulator_severity(cur, required, hle) prev_sev = file_severity.get(dest) if prev_sev is None or _SEVERITY_ORDER.get( sev, 0 diff --git a/tests/test_path_conflict_status.py b/tests/test_path_conflict_status.py index 70d834cc..846c1170 100644 --- a/tests/test_path_conflict_status.py +++ b/tests/test_path_conflict_status.py @@ -167,5 +167,61 @@ class AnExtraDoesNotPassAHashCheck(unittest.TestCase): self.assertEqual(int(packed.group(1)), 0, report.getvalue()) + +class TheEmulatorPackShipsWhatTheCoreAccepts(unittest.TestCase): + """BasiliskII wants a 512 KB to 1 MB ROM. The only ROM held was 128 KB: + the emulator pack shipped it silently and verify --emulator announced + 1/1 OK above the line saying the core would refuse it.""" + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.root = Path(self._tmp.name) + bios = self.root / "bios" / "Apple" + self.emulators = self.root / "emulators" + for directory in (bios, self.emulators): + directory.mkdir(parents=True) + payload = b"r" * 131072 + (bios / "ROM").write_bytes(payload) + sha1 = hashlib.sha1(payload).hexdigest() + files = {sha1: { + "path": str(bios / "ROM"), "name": "ROM", "size": len(payload), + "sha1": sha1, "md5": hashlib.md5(payload).hexdigest(), + "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000003", + }} + self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})} + profile = { + "emulator": "Mac", "type": "libretro", "cores": ["mac"], + "systems": ["apple-macintosh"], + "files": [{"name": "ROM", "required": True, "min_size": 524288, + "max_size": 1048576, "validation": ["size"]}], + } + (self.emulators / "mac.yml").write_text(yaml.dump(profile)) + common._emulator_profiles_cache.clear() + + def tearDown(self): + common._emulator_profiles_cache.clear() + self._tmp.cleanup() + + def test_the_refused_rom_is_named_not_shipped(self): + import zipfile + + out = self.root / "dist" + out.mkdir() + report = io.StringIO() + with contextlib.redirect_stdout(report): + zip_path = builder.generate_emulator_pack( + ["mac"], str(self.emulators), self.db, str(self.root / "bios"), + str(out), offline=True, + ) + self.assertIn("REFUSED: ROM", report.getvalue()) + with zipfile.ZipFile(zip_path) as archive: + self.assertNotIn("ROM", archive.namelist()) + + def test_verify_does_not_call_it_ok(self): + from verify import Severity, verify_emulator + + result = verify_emulator(["mac"], str(self.emulators), self.db) + self.assertEqual(result["severity_counts"][Severity.OK], 0) + if __name__ == "__main__": unittest.main()