Support compound suffix exclusions like "hash.txt" for multi-dot filenames

Agent-Logs-Url: https://github.com/rommapp/romm/sessions/d1c69638-bfa0-480e-8050-d565b234ea44

Co-authored-by: gantoine <3247106+gantoine@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-03 01:29:04 +00:00
committed by GitHub
parent 21de7e21f8
commit 55cd0cfc4f
3 changed files with 55 additions and 20 deletions

View File

@@ -205,6 +205,19 @@ class FSHandler:
match = EXTENSION_REGEX.search(file_name)
return match.group(1) if match else ""
def iter_file_extensions(self, file_name: str) -> list[str]:
"""Return all right-anchored sub-extensions for a filename.
For "game.nds.enc.hash.txt" this yields:
["nds.enc.hash.txt", "enc.hash.txt", "hash.txt", "txt"]
This allows exclusion rules like "hash.txt" to match multi-dot filenames.
"""
ext = self.parse_file_extension(file_name)
if not ext:
return []
parts = ext.split(".")
return [".".join(parts[i:]) for i in range(len(parts))]
def extract_uuid_v4_from_filename(self, file_name: str) -> str:
match = UUID_V4_REGEX.search(file_name)
return match.group(0) if match else ""
@@ -215,16 +228,11 @@ class FSHandler:
excluded_files: list[str] = []
for file_name in files:
# Get the compound extension (e.g. "nds.hash.txt" for "game.nds.hash.txt")
# and the last single extension (e.g. "txt") for multi-dot filenames.
ext = self.parse_file_extension(file_name)
suffix = Path(file_name).suffix.lstrip(".")
# Exclude the file if the compound extension or the last single extension
# is in the excluded list. Checking both handles files with multiple dots
# (e.g. "game.nds.hash.txt" should be excluded when "txt" is excluded).
if (ext and ext.lower() in excluded_extensions) or (
suffix and suffix.lower() in excluded_extensions
# Check all right-anchored sub-extensions so that rules like "hash.txt"
# match multi-dot filenames such as "game.nds.enc.hash.txt".
if any(
e.lower() in excluded_extensions
for e in self.iter_file_extensions(file_name)
):
excluded_files.append(file_name)

View File

@@ -456,12 +456,11 @@ class FSRomsHandler(FSHandler):
f"{abs_fs_path}/{rom.fs_name}", recursive=True
):
# Check if file is excluded by extension.
# Also check the last single extension for multi-dot filenames
# (e.g. "game.nds.hash.txt" should be excluded when "txt" is excluded).
ext = self.parse_file_extension(file_name)
suffix = Path(file_name).suffix.lstrip(".")
if (ext and ext.lower() in excluded_file_exts) or (
suffix and suffix.lower() in excluded_file_exts
# Check all right-anchored sub-extensions so that rules like "hash.txt"
# match multi-dot filenames such as "game.nds.enc.hash.txt".
if any(
e.lower() in excluded_file_exts
for e in self.iter_file_extensions(file_name)
):
continue

View File

@@ -145,6 +145,22 @@ class TestFSHandler:
assert handler.parse_file_extension("no_extension") == ""
assert handler.parse_file_extension("file.with.dots.txt") == "with.dots.txt"
def test_iter_file_extensions(self, handler: FSHandler):
"""Test that all right-anchored sub-extensions are returned"""
assert handler.iter_file_extensions("game.nds") == ["nds"]
assert handler.iter_file_extensions("game.nds.hash.txt") == [
"nds.hash.txt",
"hash.txt",
"txt",
]
assert handler.iter_file_extensions("game.nds.enc.hash.txt") == [
"nds.enc.hash.txt",
"enc.hash.txt",
"hash.txt",
"txt",
]
assert handler.iter_file_extensions("no_extension") == []
def test_exclude_single_files(self, handler: FSHandler):
"""Test file exclusion functionality"""
files = ["test.txt", "game.rom", "excluded.tmp", "data.json"]
@@ -162,7 +178,7 @@ class TestFSHandler:
assert "data.json" in result
def test_exclude_single_files_multi_dot(self, handler: FSHandler):
"""Test that files with multiple dots are excluded based on their last extension"""
"""Test that files with multiple dots are excluded by last or compound extension"""
files = [
"game.nds",
"game.nds.hash.txt",
@@ -172,17 +188,29 @@ class TestFSHandler:
]
with patch("handler.filesystem.base_handler.cm.get_config") as mock_config:
# Exclude by last single extension
mock_config.return_value.EXCLUDED_SINGLE_EXT = ["txt"]
mock_config.return_value.EXCLUDED_SINGLE_FILES = []
result = handler.exclude_single_files(files)
# Files with .txt as the last extension should be excluded regardless of
# how many dots are in the filename
assert "game.nds.hash.txt" not in result
assert "game.nds.enc.hash.txt" not in result
assert "readme.txt" not in result
# Non-txt files should not be excluded
assert "game.nds" in result
assert "game.rom" in result
with patch("handler.filesystem.base_handler.cm.get_config") as mock_config:
# Exclude by compound sub-extension "hash.txt"
mock_config.return_value.EXCLUDED_SINGLE_EXT = ["hash.txt"]
mock_config.return_value.EXCLUDED_SINGLE_FILES = []
result = handler.exclude_single_files(files)
assert "game.nds.hash.txt" not in result
assert "game.nds.enc.hash.txt" not in result
# "readme.txt" does NOT end in "hash.txt" — should remain
assert "readme.txt" in result
assert "game.nds" in result
assert "game.rom" in result