From 24a5acce5d4e926d3758ea02b2a425e03258dff2 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Sat, 18 Oct 2025 18:05:57 -0400 Subject: [PATCH] [ROMM-2552] Rom hashes should only include top-level nested files --- backend/handler/filesystem/roms_handler.py | 27 ++++++-- .../roms/Sonic (EU) [T]/Sonic (EU) [T].n64 | 1 + .../translation/Sonic (EU) [T-En].z64 | 1 + .../handler/filesystem/test_roms_handler.py | 66 +++++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 backend/romm_test/library/n64/roms/Sonic (EU) [T]/Sonic (EU) [T].n64 create mode 100644 backend/romm_test/library/n64/roms/Sonic (EU) [T]/translation/Sonic (EU) [T-En].z64 diff --git a/backend/handler/filesystem/roms_handler.py b/backend/handler/filesystem/roms_handler.py index 519d150f6..ae5c7621d 100644 --- a/backend/handler/filesystem/roms_handler.py +++ b/backend/handler/filesystem/roms_handler.py @@ -330,16 +330,29 @@ class FSRomsHandler(FSHandler): ): continue + # Check if this is a top-level file (not in a subdirectory) + is_top_level = f_path.samefile(Path(abs_fs_path, rom.fs_name)) + if hashable_platform: try: - crc_c, rom_crc_c, md5_h, rom_md5_h, sha1_h, rom_sha1_h = ( - self._calculate_rom_hashes( - Path(f_path, file_name), - rom_crc_c, - rom_md5_h, - rom_sha1_h, + if is_top_level: + # Include this file in the main ROM hash calculation + crc_c, rom_crc_c, md5_h, rom_md5_h, sha1_h, rom_sha1_h = ( + self._calculate_rom_hashes( + Path(f_path, file_name), + rom_crc_c, + rom_md5_h, + rom_sha1_h, + ) + ) + else: + # Calculate individual file hash only + crc_c, _, md5_h, _, sha1_h, _ = self._calculate_rom_hashes( + Path(f_path, file_name), + 0, + hashlib.md5(usedforsecurity=False), + hashlib.sha1(usedforsecurity=False), ) - ) except zlib.error: crc_c = 0 md5_h = hashlib.md5(usedforsecurity=False) diff --git a/backend/romm_test/library/n64/roms/Sonic (EU) [T]/Sonic (EU) [T].n64 b/backend/romm_test/library/n64/roms/Sonic (EU) [T]/Sonic (EU) [T].n64 new file mode 100644 index 000000000..5b290d000 --- /dev/null +++ b/backend/romm_test/library/n64/roms/Sonic (EU) [T]/Sonic (EU) [T].n64 @@ -0,0 +1 @@ +00000000 diff --git a/backend/romm_test/library/n64/roms/Sonic (EU) [T]/translation/Sonic (EU) [T-En].z64 b/backend/romm_test/library/n64/roms/Sonic (EU) [T]/translation/Sonic (EU) [T-En].z64 new file mode 100644 index 000000000..ca028fbba --- /dev/null +++ b/backend/romm_test/library/n64/roms/Sonic (EU) [T]/translation/Sonic (EU) [T-En].z64 @@ -0,0 +1 @@ +11111111 diff --git a/backend/tests/handler/filesystem/test_roms_handler.py b/backend/tests/handler/filesystem/test_roms_handler.py index 0e22d00b6..28363c803 100644 --- a/backend/tests/handler/filesystem/test_roms_handler.py +++ b/backend/tests/handler/filesystem/test_roms_handler.py @@ -46,6 +46,28 @@ class TestFSRomsHandler: full_path="n64/roms/Paper Mario (USA).z64", ) + @pytest.fixture + def rom_single_nested(self, platform: Platform): + return Rom( + id=3, + fs_name="Sonic (EU) [T]", + fs_path="n64/roms", + platform=platform, + full_path="n64/roms/Sonic (EU) [T]", + files=[ + RomFile( + id=1, + file_name="Sonic (EU) [T].n64", + file_path="n64/roms", + ), + RomFile( + id=2, + file_name="Sonic (EU) [T-En].z64", + file_path="n64/roms/translation", + ), + ], + ) + @pytest.fixture def rom_multi(self, platform: Platform): return Rom( @@ -555,3 +577,47 @@ class TestFSRomsHandler: async with await handler.stream_file("psx/roms/PaRappa the Rapper.zip") as f: content = await f.read() assert len(content) > 0 + + async def test_top_level_files_only_in_main_hash( + self, handler: FSRomsHandler, rom_single_nested + ): + """Test that only top-level files contribute to main ROM hash calculation""" + rom_files, rom_crc, rom_md5, rom_sha1, rom_ra = await handler.get_rom_files( + rom_single_nested + ) + + # Verify we have multiple files (base game + translation) + assert len(rom_files) == 2 + + base_game_rom_file = None + translation_rom_file = None + + for rom_file in rom_files: + if rom_file.file_name == "Sonic (EU) [T].n64": + base_game_rom_file = rom_file + elif rom_file.file_name == "Sonic (EU) [T-En].z64": + translation_rom_file = rom_file + + assert base_game_rom_file is not None, "Base game file not found" + assert translation_rom_file is not None, "Translation file not found" + + # Verify file categories + assert base_game_rom_file.category is None + assert translation_rom_file.category == RomFileCategory.TRANSLATION + + # The main ROM hash should be different from the translation file hash + # (this verifies that the translation is not included in the main hash) + + assert ( + rom_md5 == base_game_rom_file.md5_hash + ), "Main ROM hash should include base game file" + assert ( + rom_md5 != translation_rom_file.md5_hash + ), "Main ROM hash should not include translation file" + + assert ( + rom_sha1 == base_game_rom_file.sha1_hash + ), "Main ROM hash should include base game file" + assert ( + rom_sha1 != translation_rom_file.sha1_hash + ), "Main ROM hash should not include translation file"