From 00e99f77202acd115090a83307518ee4c77a2867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Forr=C3=B3?= Date: Wed, 11 Mar 2026 19:01:26 +0100 Subject: [PATCH 1/2] Process backslashes inside quoted shell string literals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nikola Forró Assisted-by: Claude Opus 4.6 via Cursor --- specfile/sanitizer.py | 23 +++++++++++++++++++++++ tests/unit/test_sanitizer.py | 13 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/specfile/sanitizer.py b/specfile/sanitizer.py index 8f49c34..0f93f13 100644 --- a/specfile/sanitizer.py +++ b/specfile/sanitizer.py @@ -345,6 +345,29 @@ def sanitize_shell_expansion(body: str) -> str: Sanitized shell expansion body, or %{nil} if sanitization is not possible. """ + def unescape_quoted_backslashes(s): + # process backslashes inside quoted shell string literals + try: + lex = shlex.shlex(s, posix=False) + lex.whitespace_split = True + lex.commenters = "" + parts = [] + for token in lex: + if ( + len(token) >= 2 + and token[0] in ("'", '"') + and token[-1] == token[0] + ): + inner = token[1:-1].replace("\\\\", "\\") + parts.append(token[0] + inner + token[0]) + else: + parts.append(token) + return " ".join(parts) + except ValueError: + return s + + body = unescape_quoted_backslashes(body) + def strip_quotes(s): s = s.strip() if len(s) >= 2 and s[0] in "'\"" and s[-1] == s[0]: diff --git a/tests/unit/test_sanitizer.py b/tests/unit/test_sanitizer.py index 20b41fe..4baa826 100644 --- a/tests/unit/test_sanitizer.py +++ b/tests/unit/test_sanitizer.py @@ -323,6 +323,10 @@ def test_pipe_to_tr(body, expected): "echo 1.2.3-rc4 | sed 's/-/~/g'", '%{lua:print((rpm.expand("%{quote:1.2.3-rc4}"):gsub("-", "~")))}', ), + ( + "echo %{version} | sed 's/\\\\./_/g'", + '%{lua:print((rpm.expand("%{version}"):gsub("%.", "_")))}', + ), ], ) def test_pipe_to_sed(body, expected): @@ -338,6 +342,15 @@ def test_chained_sed(): ) +def test_chained_sed_rpm_escaped(): + assert Sanitizer.sanitize_shell_expansion( + "echo '%canonical_project_name' | sed --regexp-extended 's:-:_:g;s:\\\\.:_:g'" + ) == ( + '%{lua:local v=(rpm.expand("%{canonical_project_name}"):gsub("-", "_"))' + ' print((v:gsub("%.", "_")))}' + ) + + @pytest.mark.parametrize( "body, expected", [ From 77afb3d10b427058cfc6161017954e6a15ddaff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Forr=C3=B3?= Date: Wed, 11 Mar 2026 20:12:38 +0100 Subject: [PATCH 2/2] Handle parsing errors during sanitization gracefully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nikola Forró --- specfile/sanitizer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/specfile/sanitizer.py b/specfile/sanitizer.py index 0f93f13..fad809c 100644 --- a/specfile/sanitizer.py +++ b/specfile/sanitizer.py @@ -1092,5 +1092,10 @@ def sanitize_nodes(nodes): i += 1 return "".join(result) - sanitized = sanitize_nodes(ValueParser.parse(value)) + try: + nodes = ValueParser.parse(value) + except Exception: + return "%{nil}", 0, 1 + else: + sanitized = sanitize_nodes(nodes) return sanitized, converted, removed