fix: resolve a loose archive entry by its members

gngeopi and neo-emu declare neogeo.zip loose and, apart, the ROMs they
open inside it. The archive pass took the copy holding them, the loose
entry resolved by name to another copy, and both verify --emulator and
the emulator pack reported the archive missing while the ZIP held it.
with_archive_members() gives the loose entry the members in verify, the
pack, slots and the gap report.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 05:05:54 +02:00
1 parent 348c56e94c
commit fb639647e6
5 files changed
+87 -4

No files matched your search

+2 -1
View File
@@ -94,6 +94,7 @@ from validation import (
_build_validation_index,
deliverable,
filter_files_by_mode,
with_archive_members,
)
yaml = require_yaml()
@@ -1572,7 +1573,7 @@ def generate_emulator_pack(
refused_path: str | None = None
for fe in alternatives:
local_path, status = resolve_file(
{**fe, "source_profile": emu_name},
{**with_archive_members(fe, files), "source_profile": emu_name},
db,
bios_dir,
zip_contents,
+5 -2
View File
@@ -37,7 +37,7 @@ from common import (
variant_base_name,
)
from packresolve import preferred_entries
from validation import outside_gap_scope
from validation import outside_gap_scope, with_archive_members
if TYPE_CHECKING:
from packextras import ExtraPlacement
@@ -241,7 +241,10 @@ def profile_claims(
full = placed[1]
# The owner's own copy is the one the builder ships.
local, status = resolve_local_file(
{**entry, "source_profile": emu_name},
{
**with_archive_members(entry, profile.get("files") or []),
"source_profile": emu_name,
},
db,
zip_contents,
dest_hint=dest,
+15
View File
@@ -13,6 +13,7 @@ from collections.abc import Callable, Iterator, Mapping
from common import (
adler32_of,
archive_members,
compute_hashes,
name_match_size_ok,
sanitize_pack_path,
@@ -512,6 +513,20 @@ def declaration_settles(
return False
def with_archive_members(entry: dict, files: list[dict]) -> dict:
"""A loose entry resolved as the archive its profile reads members from.
gngeopi declares neogeo.zip and, apart, the ROMs it opens inside it. The
members decide which same-named copy is the archive: resolved by name
alone, the loose entry took another copy than the archive pass and the
profile blocked its own archive.
"""
if entry.get("archive"):
return entry
members = archive_members(entry.get("name", ""), files)
return {**entry, "members": members} if members else entry
def opens_by_name(file_entry: dict, profile: dict, standalone: bool = False) -> bool:
"""Whether the code opens this entry by its name.
+5 -1
View File
@@ -100,6 +100,7 @@ from validation import (
installed_with_emulator,
unread_on,
outside_gap_scope,
with_archive_members,
)
DEFAULT_DB = "database.json"
@@ -683,6 +684,9 @@ def find_undeclared_files(
}
if f.get("contents"):
entry["contents"] = f["contents"]
members = with_archive_members(f, profile.get("files", [])).get("members")
if members:
entry["members"] = members
if launcher_layer:
launcher_candidates.append((emu_name, is_standalone, f, entry))
if duplicate:
@@ -1887,7 +1891,7 @@ def verify_emulator(
dest_hint = file_entry.get("path", "")
local_path, resolve_status = resolve_local_file(
{**file_entry, "source_profile": emu_name},
{**with_archive_members(file_entry, files), "source_profile": emu_name},
db,
zip_contents,
dest_hint=dest_hint,
+60
View File
@@ -349,5 +349,65 @@ class AnArchiveRankedByWhatItHolds(unittest.TestCase):
self.assertIn('entry["contents"] = f["contents"]', report)
class ALooseEntryNamingItsArchive(unittest.TestCase):
"""gngeopi declares neogeo.zip loose and, apart, the ROMs it opens inside.
The archive pass took the copy holding them, the loose entry resolved by
name alone to another copy, and the profile blocked its own archive:
verify said missing, the pack said missing, and the ZIP held it."""
def test_verify_and_the_pack_ship_the_archive_once(self):
import io
from contextlib import redirect_stdout
import yaml
from common import _emulator_profiles_cache
from generate_pack import generate_emulator_pack
from verify import verify_emulator
_emulator_profiles_cache.clear()
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, root, True)
previous = os.getcwd()
os.chdir(root)
self.addCleanup(os.chdir, previous)
roms = {"bios.rom": b"B" * 32, "lo.lo": b"L" * 32, "rev.rom": b"R" * 32}
files, by_name = {}, {}
# Two revisions, neither holding the other: index order picks A.
for path, members in (("bios/A/set.zip", ["bios.rom", "rev.rom"]),
("bios/B/set.zip", ["bios.rom", "lo.lo"])):
Path(path).parent.mkdir(parents=True)
with zipfile.ZipFile(path, "w") as zf:
for member in members:
zf.writestr(member, roms[member])
sha1 = hashlib.sha1(Path(path).read_bytes()).hexdigest()
files[sha1] = {"path": path, "name": "set.zip", "sha1": sha1, "size": 1}
by_name.setdefault("set.zip", []).append(sha1)
db = {"files": files, "indexes": {"by_name": by_name, "by_md5": {},
"by_path_suffix": {}, "by_crc32": {}}}
Path("emulators").mkdir()
Path("emulators/neo.yml").write_text(yaml.safe_dump({
"emulator": "Neo", "type": "libretro", "systems": ["snk-neogeo-mvs"],
"files": [{"name": "set.zip", "required": True, "category": "bios_zip"}] + [
{"name": name, "archive": "set.zip", "required": True,
"crc32": format(zlib.crc32(data), "08x")}
for name, data in roms.items() if name != "rev.rom"
],
}))
report = verify_emulator(["neo"], "emulators", db, platforms_dir="platforms")
with redirect_stdout(io.StringIO()) as out:
zip_path = generate_emulator_pack(
["neo"], "emulators", db, "bios", "out", offline=True,
platforms_dir="platforms",
)
with zipfile.ZipFile(zip_path) as zf, zf.open("set.zip") as inner:
held = zipfile.ZipFile(io.BytesIO(inner.read())).namelist()
self.assertEqual(sorted(held), ["bios.rom", "lo.lo"])
self.assertNotIn("missing", out.getvalue())
self.assertEqual({d["status"] for d in report["details"]}, {"ok"})
self.assertEqual(report["total_files"], 1)
if __name__ == "__main__":
unittest.main()