From 97c960484c44c540ae4085bba8eb3f5fcdc8b36d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:05:54 +0200 Subject: [PATCH] fix: read the file a detected literal names A printf conversion glued to a name, a name with spaces cut to its last word, an extension list, and an identifier in code were reported as new files. --- scripts/profile_sync.py | 86 +++++++++++++++++++++++++++++++++++--- tests/test_profile_sync.py | 38 +++++++++++++++++ 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index a830d3f5..4b130495 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -2300,23 +2300,95 @@ def declared_hashes(profile: dict) -> set[str]: return hashes +# Sources where a filename is a string. Outside a literal or a comment, +# `EE.bios` is a config key and `psm_s.pal` a struct field (pcee2). +CODE_SUFFIXES = ( + ".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inc", ".inl", + ".m", ".mm", ".rs", ".java", ".kt", ".cs", ".swift", +) +_PRINTF_SPEC_RE = re.compile( + r"[-+ #0]*(?:\d+|\*)?(?:\.\d+)?(?:hh|h|ll|l|z|j|t|L)?[diouxXsScpfFeEgGaA]" +) +_TITLE_WORD_RE = re.compile(r"[A-Z0-9][\w.()&'+-]*") + + +def _string_literal(line: str, index: int) -> tuple[int, int] | None: + """Bounds of the double-quoted literal holding index, None outside one.""" + start = None + position = 0 + while position < len(line): + char = line[position] + if char == "\\" and start is not None: + position += 2 + continue + if char == '"': + if start is None: + start = position + 1 + else: + if start <= index < position: + return start, position + start = None + position += 1 + return None + + +def _in_comment(line: str, index: int) -> bool: + before = line[:index] + return "//" in before or "/*" in before or line.lstrip().startswith(("*", "#")) + + +def file_literal(line: str, start: int, end: int, code: bool) -> str | None: + """The filename a match stands for, None when it names no file. + + A printf conversion glued to the name is not part of it: sdl2trs writes + `"%slevel2.rom"` with ROM_PATH in front. A name with spaces is read + whole: virtualjaguar opens `"Jaguar CD BIOS.rom"`, and the match alone + was `BIOS.rom`. Such a name is written as a title, so `"Please insert + bios.bin"` and `"Packing patches.zip"` still yield the file alone. In + a code file a match outside any string or comment is an identifier and + names nothing. + """ + name = line[start:end] + if start and line[start - 1] == "%": + spec = _PRINTF_SPEC_RE.match(name) + if spec: + name = name[spec.end():] + literal = _string_literal(line, start) + if literal is None: + if code and not _in_comment(line, start): + return None + elif literal[1] == end and line[start - 1:start] == " ": + words = line[literal[0]:start].split() + [name] + if all(_TITLE_WORD_RE.fullmatch(word) for word in words): + return line[literal[0]:end] + base = os.path.basename(name) + # `".bin.hex.iii.rom"` (sdl2trs) lists extensions, it names no file. + if base.startswith(".") or not _FILE_RE.fullmatch(base): + return None + return base + + def detect_new_files( - head_lines: list[str], known: set[str], cap: int = 25 + head_lines: list[str], known: set[str], cap: int = 25, path: str = "" ) -> tuple[list[tuple[int, str]], int]: """Filename literals at HEAD the profile does not declare.""" found: list[tuple[int, str]] = [] seen: set[str] = set() elided = 0 + code = path.lower().endswith(CODE_SUFFIXES) for number, line in enumerate(head_lines, start=1): - for match in _FILE_RE.findall(line): - name = os.path.basename(match).casefold() + for match in _FILE_RE.finditer(line): + literal = file_literal(line, match.start(), match.end(), code) + if literal is None: + continue + name = literal.casefold() if name in known or name in seen: continue seen.add(name) if len(found) >= cap: elided += 1 continue - found.append((number, os.path.basename(match))) + found.append((number, literal)) return found, elided @@ -4050,10 +4122,12 @@ def _detect( or [] ) if args.detect_new_files: - found, elided = detect_new_files(head_lines, names) + found, elided = detect_new_files(head_lines, names, path=path) at_pin = { name.casefold() - for _, name in detect_new_files(pin_lines, names, cap=sys.maxsize)[0] + for _, name in detect_new_files( + pin_lines, names, cap=sys.maxsize, path=path + )[0] } new_files.extend( {"path": path, "line": number, "name": name, diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 96f7282d..d728fac9 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -3310,6 +3310,44 @@ class TestDetectNewFiles(unittest.TestCase): found, _ = detect_new_files(['#include "driver.cpp"'], set()) self.assertEqual(found, []) + def test_a_printf_conversion_is_not_part_of_the_name(self): + # sdl2trs src/trs_options.c:864-866 builds "%slevel2.rom" and the + # report named slevel2.rom. + lines = [ + 'snprintf(romfile1, sizeof(romfile1), "%slevel2.rom", ROM_PATH);', + 'snprintf(name, 64, "%02dcart.bin", slot);', + ] + found, _ = detect_new_files(lines, set(), path="src/trs_options.c") + self.assertEqual(found, [(1, "level2.rom"), (2, "cart.bin")]) + + def test_a_quoted_name_with_spaces_is_read_whole(self): + # virtualjaguar libretro.c:5815-5816 opens "Jaguar CD BIOS.rom", + # reported as BIOS.rom. A sentence still yields the name it holds. + lines = [ + 'snprintf(path, n, "%s%c%s", dir, slash, "Jaguar CD BIOS.rom");', + 'log("Please insert bios.bin first");', + 'message(STATUS "Packing patches.zip")', + '#define ROM ".bin.hex.iii.rom"', + ] + found, _ = detect_new_files(lines, set(), path="libretro.c") + self.assertEqual( + found, + [(1, "Jaguar CD BIOS.rom"), (2, "bios.bin"), (3, "patches.zip")], + ) + + def test_an_identifier_in_code_is_not_a_file(self): + # pcee2 reads config keys EE.bios and IOP.bios and a field psm_s.pal. + lines = [ + "cfg.EE.bios = load(path);", + "if (psm_s.pal) {", + "// loads boot.rom from the system directory", + 'open("ipl.rom");', + ] + found, _ = detect_new_files(lines, set(), path="src/core/config.cpp") + self.assertEqual(found, [(3, "boot.rom"), (4, "ipl.rom")]) + found, _ = detect_new_files(["Copy EE.bios here"], set(), path="README.md") + self.assertEqual(found, [(1, "EE.bios")]) + class TestWatchHashes(unittest.TestCase): def test_finds_new_sha1(self):