mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-17 17:03:27 -05:00
fix: name long emulator packs within a file name
--system snes joined 23 emulator names into the ZIP name and died with ENAMETOOLONG before writing anything. The system pack now passes its own name in, and an --emulator selection too long to join keeps its first name, a count and a digest of the selection.
This commit is contained in:
1 parent
bf47d3d998
commit
29fb1a58e4
2 files changed
+63
-16
No files matched your search
+43
-16
@@ -1209,6 +1209,25 @@ def _same_bytes(path: str, other: str | None) -> bool:
|
||||
return compute_hashes(path)["sha1"] == compute_hashes(other)["sha1"]
|
||||
|
||||
|
||||
# A file name holds 255 bytes on every filesystem a pack is written to;
|
||||
# the margin leaves room for the split part suffix and a scratch prefix.
|
||||
MAX_PACK_NAME_BYTES = 200
|
||||
|
||||
|
||||
def _bounded_pack_name(display_names: list[str], keys: list[str], suffix: str) -> str:
|
||||
"""The emulator pack's file name, its emulators joined while they fit.
|
||||
|
||||
Past the limit the name keeps the first emulator, says how many more the
|
||||
pack holds, and carries a digest of the selection so two different
|
||||
selections never share a file.
|
||||
"""
|
||||
joined = "_".join(display_names) + suffix
|
||||
if len(joined.encode("utf-8")) <= MAX_PACK_NAME_BYTES:
|
||||
return joined
|
||||
digest = hashlib.sha1("\n".join(sorted(keys)).encode("utf-8")).hexdigest()[:8]
|
||||
return f"{display_names[0]}_and_{len(display_names) - 1}_more_{digest}{suffix}"
|
||||
|
||||
|
||||
def generate_emulator_pack(
|
||||
profile_names: list[str],
|
||||
emulators_dir: str,
|
||||
@@ -1221,8 +1240,14 @@ def generate_emulator_pack(
|
||||
regions: list[str] | None = None,
|
||||
offline: bool | None = None,
|
||||
platforms_dir: str | None = None,
|
||||
pack_name: str | None = None,
|
||||
) -> str | None:
|
||||
"""Generate a ZIP pack for specific emulator profiles."""
|
||||
"""Generate a ZIP pack for specific emulator profiles.
|
||||
|
||||
`pack_name` is the caller's own file name: the system pack names its
|
||||
archive after the system, and joining 23 SNES emulator names went past
|
||||
the 255 bytes a file name may hold.
|
||||
"""
|
||||
all_profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
|
||||
if zip_contents is None:
|
||||
zip_contents = build_zip_contents_index(db)
|
||||
@@ -1242,7 +1267,9 @@ def generate_emulator_pack(
|
||||
"full", regions, None, False, required_only, standalone
|
||||
)
|
||||
)
|
||||
zip_name = "_".join(display_names) + f"{narrow_tags}_BIOS_Pack.zip"
|
||||
zip_name = pack_name or _bounded_pack_name(
|
||||
display_names, [n for n, _p in selected], f"{narrow_tags}_BIOS_Pack.zip"
|
||||
)
|
||||
zip_path = os.path.join(output_dir, zip_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
@@ -1595,6 +1622,18 @@ def generate_system_pack(
|
||||
# Built in a scratch directory: under its emulator name in output_dir it
|
||||
# overwrote, then carried away, an --emulator pack already there.
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
# Every dimension goes through the one list: keeping only the region
|
||||
# tag made --system X --required-only overwrite the pack built without it.
|
||||
tags = "".join(
|
||||
tag
|
||||
for tag, _label in _narrowings(
|
||||
"full", regions, None, False, required_only, standalone=standalone
|
||||
)
|
||||
)
|
||||
# The selection mode is part of the name: `--system sega-triforce`
|
||||
# and `--emulator triforce` both wrote Triforce_BIOS_Pack.zip, and
|
||||
# the second run replaced the first without a word.
|
||||
final_name = f"{sys_display}_System{tags}_BIOS_Pack.zip"
|
||||
scratch = tempfile.mkdtemp(prefix=".system-", dir=output_dir)
|
||||
try:
|
||||
result = generate_emulator_pack(
|
||||
@@ -1609,23 +1648,11 @@ def generate_system_pack(
|
||||
regions=regions,
|
||||
offline=offline,
|
||||
platforms_dir=platforms_dir,
|
||||
pack_name=final_name,
|
||||
)
|
||||
if not result:
|
||||
return None
|
||||
# Every dimension goes through the one list: keeping only the region
|
||||
# tag made --system X --required-only overwrite the pack built without it.
|
||||
tags = "".join(
|
||||
tag
|
||||
for tag, _label in _narrowings(
|
||||
"full", regions, None, False, required_only, standalone=standalone
|
||||
)
|
||||
)
|
||||
# The selection mode is part of the name: `--system sega-triforce`
|
||||
# and `--emulator triforce` both wrote Triforce_BIOS_Pack.zip, and
|
||||
# the second run replaced the first without a word.
|
||||
new_path = os.path.join(
|
||||
output_dir, f"{sys_display}_System{tags}_BIOS_Pack.zip"
|
||||
)
|
||||
new_path = os.path.join(output_dir, final_name)
|
||||
os.replace(result, new_path)
|
||||
return new_path
|
||||
finally:
|
||||
|
||||
@@ -2187,6 +2187,26 @@ class ContributorsSurviveAFailedRequest(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class APackNameFitsAFileName(unittest.TestCase):
|
||||
"""--system snes joined 23 emulator names and died on ENAMETOOLONG
|
||||
before writing anything, while verify --system snes reported."""
|
||||
|
||||
def test_a_long_selection_is_bounded_and_told_apart(self):
|
||||
from scripts.generate_pack import MAX_PACK_NAME_BYTES, _bounded_pack_name
|
||||
|
||||
names = [f"Emulator_Number_{i}" for i in range(23)]
|
||||
long_name = _bounded_pack_name(names, names, "_BIOS_Pack.zip")
|
||||
self.assertLessEqual(len(long_name.encode()), MAX_PACK_NAME_BYTES)
|
||||
self.assertTrue(long_name.startswith("Emulator_Number_0_and_22_more_"))
|
||||
other = _bounded_pack_name(names, names[:-1] + ["x"], "_BIOS_Pack.zip")
|
||||
self.assertNotEqual(long_name, other)
|
||||
self.assertEqual(_bounded_pack_name(["A", "B"], ["a", "b"], ".zip"), "A_B.zip")
|
||||
|
||||
def test_the_system_pack_names_itself(self):
|
||||
source = (ROOT / "scripts" / "generate_pack.py").read_text(encoding="utf-8")
|
||||
self.assertIn("pack_name=final_name", source)
|
||||
|
||||
|
||||
class TheDatabaseCacheIsWrittenWhole(unittest.TestCase):
|
||||
"""A crash while serializing left a truncated db_cache.json behind, and
|
||||
the next run rehashed the whole collection from scratch."""
|
||||
|
||||
Reference in new issue
Block a user