Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 124 additions & 126 deletions specfile/sanitizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,59 +91,6 @@
_LUA_STRING_LITERAL_RE = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')


def _strip_lua_comments(code):
"""Strip Lua comments while preserving string literals.

Processes code left-to-right so that ``--`` inside a quoted string
is never mistaken for a comment start.
"""
result = []
i = 0
while i < len(code):
if code[i] in ('"', "'"):
quote = code[i]
result.append(code[i])
i += 1
while i < len(code) and code[i] != quote:
if code[i] == "\\" and i + 1 < len(code):
result.append(code[i : i + 2])
i += 2
else:
result.append(code[i])
i += 1
if i < len(code):
result.append(code[i])
i += 1
elif code[i : i + 2] == "--":
j = i + 2
if j < len(code) and code[j] == "[":
level = 0
k = j + 1
while k < len(code) and code[k] == "=":
level += 1
k += 1
if k < len(code) and code[k] == "[":
close = "]" + "=" * level + "]"
end = code.find(close, k + 1)
if end != -1:
i = end + len(close)
else:
i = len(code)
result.append(" ")
continue
end = code.find("\n", i)
if end != -1:
result.append(" ")
i = end
else:
result.append(" ")
i = len(code)
else:
result.append(code[i])
i += 1
return "".join(result)


_UNSAFE_LUA_BRACKET_RE = re.compile(r"\[\s*(?![#\d])")

_UNSAFE_LUA_STRING_CONTENT_RE = re.compile(
Expand All @@ -154,67 +101,6 @@ def _strip_lua_comments(code):
_EXPRESSION_LUA_PREFIX_RE = re.compile(r"lua\s*:")


def _decode_lua_escapes(s):
"""Decode all Lua string escape sequences to their character values."""
_SIMPLE = {
"a": "\a",
"b": "\b",
"f": "\f",
"n": "\n",
"r": "\r",
"t": "\t",
"v": "\v",
"\\": "\\",
'"': '"',
"'": "'",
}
result = []
i = 0
while i < len(s):
if s[i] != "\\" or i + 1 >= len(s):
result.append(s[i])
i += 1
continue
c = s[i + 1]
if c in _SIMPLE:
result.append(_SIMPLE[c])
i += 2
elif c == "z":
i += 2
while i < len(s) and s[i] in " \t\n\r":
i += 1
elif c == "x" and i + 3 < len(s):
try:
result.append(chr(int(s[i + 2 : i + 4], 16)))
i += 4
except ValueError:
result.append(s[i])
i += 1
elif c == "u" and i + 2 < len(s) and s[i + 2] == "{":
end = s.find("}", i + 3)
if end != -1:
try:
result.append(chr(int(s[i + 3 : end], 16)))
i = end + 1
except (ValueError, OverflowError):
result.append(s[i])
i += 1
else:
result.append(s[i])
i += 1
elif c.isdigit():
j = i + 1
while j < len(s) and j < i + 4 and s[j].isdigit():
j += 1
num = int(s[i + 1 : j])
result.append(chr(num % 256))
i = j
else:
result.append(s[i])
i += 1
return "".join(result)


_UNSAFE_LUA_IDENTIFIERS = frozenset(
{
"_G",
Expand Down Expand Up @@ -287,7 +173,7 @@ def sanitize_shell_expansion(body: str) -> str:
or Lua expressions. Covered patterns are:

- Substring extraction:
`%(c=%{commit}; echo ${c:0:7})` → `%{sub %{commit}, 1, 7}`
`%(c=%{commit}; echo ${c:0:7})` → `%{sub %{commit} 1 7}`
- Bash string replacement:
`%(v=%{version}; echo ${v//./_})` → `%{lua:
print((rpm.expand("%{version}"):gsub("%.", "_")))}`
Expand Down Expand Up @@ -331,7 +217,7 @@ def sanitize_shell_expansion(body: str) -> str:
`%(test "%{_libdir}" != "%{_prefix}/lib" && echo 1 || echo 0)` → `%{lua:
print(rpm.expand("%{_libdir}") ~= rpm.expand("%{_prefix}/lib") and "1" or "0")}`
- Printf truncation:
`%(printf %%.7s %commit)` → `%{sub %{commit}, 1, 7}`
`%(printf %%.7s %commit)` → `%{sub %{commit} 1 7}`
- Printf float formatting:
`%(LANG=C printf "%.4f" %{cpan_ver})` → `%{lua:
print(string.format("%%.4f", tonumber(rpm.expand("%{cpan_ver}"))))}`
Expand Down Expand Up @@ -567,8 +453,8 @@ def convert_string_op(expr, cmd):
mode, start, end, delim = cut
if mode == "bytes":
if end is not None:
return f"%{{sub {expr}, {start}, {end}}}"
return f"%{{sub {expr}, {start}}}"
return f"%{{sub {expr} {start} {end}}}"
return f"%{{sub {expr} {start}}}"
elif mode == "field":
return build_lua_field(expr, delim, start)
elif mode == "range":
Expand Down Expand Up @@ -700,14 +586,14 @@ def convert_glob_removal(expr, op, pat):
start = offset + 1
if length_raw is not None:
if length_raw.isdigit():
return f"%{{sub {expr}, {start}, {offset + int(length_raw)}}}"
return f"%{{sub {expr} {start} {offset + int(length_raw)}}}"
length_macro = normalize_macro(length_raw)
if length_macro is not None:
if offset == 0:
return f"%{{sub {expr}, {start}, {length_macro}}}"
return f"%{{sub {expr}, {start}, %[{length_macro} + {offset}]}}"
return f"%{{sub {expr} {start} {length_macro}}}"
return f"%{{sub {expr} {start} %[{length_macro} + {offset}]}}"
else:
return f"%{{sub {expr}, {start}}}"
return f"%{{sub {expr} {start}}}"

# --- var=macro; echo ${var//PAT/REPL} → Lua gsub ---
m = _RE_BASH_REPLACE.match(body)
Expand Down Expand Up @@ -850,13 +736,13 @@ def convert_glob_removal(expr, op, pat):
f' and "{lua_string_escape(a)}" or "{lua_string_escape(b)}")}}'
)

# --- printf %.Ns MACRO → %{sub MACRO, 1, N} ---
# --- printf %.Ns MACRO → %{sub MACRO 1 N} ---
m = _RE_PRINTF_TRUNC.match(body)
if m:
n = int(m.group(1))
expr = normalize_macro(m.group(2))
if expr is not None:
return f"%{{sub {expr}, 1, {n}}}"
return f"%{{sub {expr} 1 {n}}}"

# --- printf %.Nf MACRO → Lua string.format ---
m = _RE_PRINTF_FLOAT.match(body)
Expand Down Expand Up @@ -890,6 +776,118 @@ def convert_glob_removal(expr, op, pat):

@staticmethod
def is_lua_safe(code):
def strip_lua_comments(code):
"""Strip Lua comments while preserving string literals.

Processes code left-to-right so that ``--`` inside a quoted string
is never mistaken for a comment start.
"""
result = []
i = 0
while i < len(code):
if code[i] in ('"', "'"):
quote = code[i]
result.append(code[i])
i += 1
while i < len(code) and code[i] != quote:
if code[i] == "\\" and i + 1 < len(code):
result.append(code[i : i + 2])
i += 2
else:
result.append(code[i])
i += 1
if i < len(code):
result.append(code[i])
i += 1
elif code[i : i + 2] == "--":
j = i + 2
if j < len(code) and code[j] == "[":
level = 0
k = j + 1
while k < len(code) and code[k] == "=":
level += 1
k += 1
if k < len(code) and code[k] == "[":
close = "]" + "=" * level + "]"
end = code.find(close, k + 1)
if end != -1:
i = end + len(close)
else:
i = len(code)
result.append(" ")
continue
end = code.find("\n", i)
if end != -1:
result.append(" ")
i = end
else:
result.append(" ")
i = len(code)
else:
result.append(code[i])
i += 1
return "".join(result)

def decode_lua_escapes(s):
"""Decode all Lua string escape sequences to their character values."""
_SIMPLE = {
"a": "\a",
"b": "\b",
"f": "\f",
"n": "\n",
"r": "\r",
"t": "\t",
"v": "\v",
"\\": "\\",
'"': '"',
"'": "'",
}
Comment on lines +833 to +844

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This _SIMPLE dictionary is constant and is being recreated on every call to decode_lua_escapes, which is called inside a loop. For a minor performance improvement, you can define it once outside this function, at the is_lua_safe method's scope.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe later.

result = []
i = 0
while i < len(s):
if s[i] != "\\" or i + 1 >= len(s):
result.append(s[i])
i += 1
continue
c = s[i + 1]
if c in _SIMPLE:
result.append(_SIMPLE[c])
i += 2
elif c == "z":
i += 2
while i < len(s) and s[i] in " \t\n\r":
i += 1
elif c == "x" and i + 3 < len(s):
try:
result.append(chr(int(s[i + 2 : i + 4], 16)))
i += 4
except ValueError:
result.append(s[i])
i += 1
elif c == "u" and i + 2 < len(s) and s[i + 2] == "{":
end = s.find("}", i + 3)
if end != -1:
try:
result.append(chr(int(s[i + 3 : end], 16)))
i = end + 1
except (ValueError, OverflowError):
result.append(s[i])
i += 1
else:
result.append(s[i])
i += 1
elif c.isdigit():
j = i + 1
while j < len(s) and j < i + 4 and s[j].isdigit():
j += 1
num = int(s[i + 1 : j])
result.append(chr(num % 256))
i = j
else:
result.append(s[i])
i += 1
return "".join(result)

def has_safe_format_specs(fmt):
"""
Check that a format string only uses safe specifiers.
Expand All @@ -903,13 +901,13 @@ def has_safe_format_specs(fmt):
cleaned = _SAFE_FORMAT_SPEC_RE.sub("", expanded)
return "%" not in cleaned

stripped = _strip_lua_comments(code)
stripped = strip_lua_comments(code)
string_spans = []
for m in _LUA_STRING_LITERAL_RE.finditer(stripped):
content = m.group(0)[1:-1]
if _UNSAFE_LUA_STRING_CONTENT_RE.search(content):
return False
decoded = _decode_lua_escapes(content)
decoded = decode_lua_escapes(content)
if _UNSAFE_LUA_STRING_CONTENT_RE.search(decoded):
return False
if decoded.endswith("%"):
Expand Down
Loading
Loading