Skip to content

Commit a7d535e

Browse files
Try to prevent sideeffects during macro expansion (#499)
Try to prevent sideeffects during macro expansion Macro expansion can have sideeffects and modify other macros, which becomes a problem when the spec file is processed line by line to determine line validity. Even lines in false condition branches are expanded, and if the expansion has sideeffects it can affect further processing and condition evaluation. Try to prevent that by temporarily overriding macros that can affect other macros, making them noop during expansion. Unfortunately overriding doesn't work on EL9 and EL8 where RPM behaves differently. Fixes #498. Reviewed-by: gemini-code-assist[bot] Reviewed-by: Nikola Forró Reviewed-by: Maja Massarini
2 parents 43dbec6 + 2662dad commit a7d535e

File tree

2 files changed

+39
-6
lines changed

2 files changed

+39
-6
lines changed

specfile/macros.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: MIT
33

44
import collections
5+
import functools
56
import re
67
from enum import IntEnum
78
from typing import List, Optional
@@ -131,24 +132,36 @@ def dump(cls) -> List[Macro]:
131132
return cls._parse([line.decode() for line in stderr])
132133

133134
@staticmethod
134-
def expand(expression: str) -> str:
135+
def expand(expression: str, safe: bool = True) -> str:
135136
"""
136137
Expands an expression in the global context.
137138
138139
Args:
139140
expression: Expression to expand.
141+
safe: Whether to disable macro manipulation during expansion.
140142
141143
Returns:
142144
Expanded expression.
143145
144146
Raises:
145147
RPMException: If expansion error occurs.
146148
"""
149+
cleanups = []
150+
if safe:
151+
# override macros that can affect other macros
152+
# this should avoid any side-effects caused by the expansion
153+
for macro in ("global", "define", "undefine"):
154+
rpm.addMacro(macro, "%{nil}")
155+
cleanups.append(functools.partial(rpm.delMacro, macro))
147156
try:
148-
with capture_stderr() as stderr:
149-
return rpm.expandMacro(expression)
150-
except rpm.error as e:
151-
raise RPMException(stderr=stderr) from e
157+
try:
158+
with capture_stderr() as stderr:
159+
return rpm.expandMacro(expression)
160+
except rpm.error as e:
161+
raise RPMException(stderr=stderr) from e
162+
finally:
163+
while cleanups:
164+
cleanups.pop(0)()
152165

153166
@classmethod
154167
def remove(cls, macro: str) -> None:
@@ -169,7 +182,10 @@ def remove(cls, macro: str) -> None:
169182
while retry < MAX_REMOVAL_RETRIES:
170183
rpm.delMacro(macro)
171184
try:
172-
if cls.expand(f"%{{{macro}}}") == f"%{{{macro.replace('%%', '%')}}}":
185+
if (
186+
cls.expand(f"%{{{macro}}}", safe=False)
187+
== f"%{{{macro.replace('%%', '%')}}}"
188+
):
173189
break
174190
except RPMException:
175191
# the macro can't be expanded, but it still exists

tests/unit/test_macros.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,20 @@ def test_macros_define():
114114
def test_macros_reinit():
115115
Macros.reinit(MacroLevel.BUILTIN)
116116
assert all(m.level == MacroLevel.BUILTIN for m in Macros.dump())
117+
118+
119+
@pytest.mark.xfail(
120+
rpm.__version__ <= "4.16.1.3",
121+
reason="rpm <= 4.16.1.3 treats builtin macros specially and overriding them has no effect",
122+
)
123+
def test_macros_sideeffects():
124+
rpm.reloadConfig()
125+
rpm.addMacro("with_feature", "1")
126+
assert rpm.expandMacro("%with_feature") == "1"
127+
Macros.expand("%{expand:%global with_feature 0}", safe=False)
128+
assert rpm.expandMacro("%with_feature") == "0"
129+
rpm.reloadConfig()
130+
rpm.addMacro("with_feature", "1")
131+
assert rpm.expandMacro("%with_feature") == "1"
132+
Macros.expand("%{expand:%global with_feature 0}", safe=True)
133+
assert rpm.expandMacro("%with_feature") == "1"

0 commit comments

Comments
 (0)