Skip to content

Commit 2bdcec3

Browse files
Fix tokenization of option strings (#253)
Fix tokenization of option strings Adjust the tokenization algorithm to take into account quotes contained within macro expansions and other expressions. Notes for reviewers: The tokenization works as before, but the option string is first parsed into nodes and everything that's not a string literal is treated as a single token. RELEASE NOTES BEGIN Fixed a bug in processing options of %prep macros. For instance, when a quoted string appeared inside an expression expansion, it could lead to improper parsing, rendering the spec file invalid after accessing the options. RELEASE NOTES END Reviewed-by: Laura Barcziová
2 parents 2572ebe + b5a1ce7 commit 2bdcec3

File tree

2 files changed

+83
-55
lines changed

2 files changed

+83
-55
lines changed

specfile/options.py

Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from specfile.exceptions import OptionsException
1111
from specfile.formatter import formatted
12+
from specfile.value_parser import Node, StringLiteral, ValueParser
1213

1314

1415
class TokenType(Enum):
@@ -474,63 +475,80 @@ def tokenize(option_string: str) -> List[Token]:
474475
Raises:
475476
OptionsException if the option string is untokenizable.
476477
"""
477-
result = []
478-
token = ""
479-
quote = None
480-
inp = list(option_string)
481-
while inp:
482-
c = inp.pop(0)
483-
if c == quote:
484-
if token:
485-
result.append(
486-
Token(
487-
TokenType.QUOTED
488-
if quote == "'"
489-
else TokenType.DOUBLE_QUOTED,
490-
token,
491-
)
492-
)
493-
token = ""
478+
result: List[Token] = []
479+
480+
def append_default(s):
481+
if result and result[-1].type == TokenType.DEFAULT:
482+
result[-1].value += s
483+
else:
484+
result.append(Token(TokenType.DEFAULT, s))
485+
486+
token_nodes: List[Node] = []
487+
for node in ValueParser.parse(option_string):
488+
if isinstance(node, StringLiteral):
489+
if token_nodes:
490+
append_default("".join(str(n) for n in token_nodes))
491+
token_nodes = []
492+
token = ""
494493
quote = None
495-
continue
496-
if quote:
497-
if c == "\\":
498-
if not inp:
499-
raise OptionsException("No escaped character")
500-
c = inp.pop(0)
501-
if c != quote:
502-
token += "\\"
503-
token += c
504-
continue
505-
if c.isspace():
506-
if token:
507-
result.append(Token(TokenType.DEFAULT, token))
508-
token = ""
509-
whitespace = c
494+
inp = list(str(node))
510495
while inp:
511496
c = inp.pop(0)
512-
if not c.isspace():
513-
break
514-
whitespace += c
515-
else:
516-
result.append(Token(TokenType.WHITESPACE, whitespace))
517-
break
518-
inp.insert(0, c)
519-
result.append(Token(TokenType.WHITESPACE, whitespace))
520-
continue
521-
if c in ('"', "'"):
497+
if c == quote:
498+
if token:
499+
result.append(
500+
Token(
501+
TokenType.QUOTED
502+
if quote == "'"
503+
else TokenType.DOUBLE_QUOTED,
504+
token,
505+
)
506+
)
507+
token = ""
508+
quote = None
509+
continue
510+
if quote:
511+
if c == "\\":
512+
if not inp:
513+
raise OptionsException("No escaped character")
514+
c = inp.pop(0)
515+
if c != quote:
516+
token += "\\"
517+
token += c
518+
continue
519+
if c.isspace():
520+
if token:
521+
append_default(token)
522+
token = ""
523+
whitespace = c
524+
while inp:
525+
c = inp.pop(0)
526+
if not c.isspace():
527+
break
528+
whitespace += c
529+
else:
530+
result.append(Token(TokenType.WHITESPACE, whitespace))
531+
break
532+
inp.insert(0, c)
533+
result.append(Token(TokenType.WHITESPACE, whitespace))
534+
continue
535+
if c in ('"', "'"):
536+
if token:
537+
append_default(token)
538+
token = ""
539+
quote = c
540+
continue
541+
if c == "\\":
542+
if not inp:
543+
raise OptionsException("No escaped character")
544+
c = inp.pop(0)
545+
token += c
546+
if quote:
547+
raise OptionsException("No closing quotation")
522548
if token:
523-
result.append(Token(TokenType.DEFAULT, token))
524-
token = ""
525-
quote = c
526-
continue
527-
if c == "\\":
528-
if not inp:
529-
raise OptionsException("No escaped character")
530-
c = inp.pop(0)
531-
token += c
532-
if quote:
533-
raise OptionsException("No closing quotation")
534-
if token:
535-
result.append(Token(TokenType.DEFAULT, token))
549+
append_default(token)
550+
else:
551+
token_nodes.append(node)
552+
if token_nodes:
553+
append_default("".join(str(n) for n in token_nodes))
536554
return result

tests/unit/test_options.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,16 @@ def test_options_find_option(optstring, tokens, option, result):
248248
Token(TokenType.WHITESPACE, " "),
249249
],
250250
),
251+
(
252+
'-q -n %{name}-%{version}%[%{rc}?"-rc":""]',
253+
[
254+
Token(TokenType.DEFAULT, "-q"),
255+
Token(TokenType.WHITESPACE, " "),
256+
Token(TokenType.DEFAULT, "-n"),
257+
Token(TokenType.WHITESPACE, " "),
258+
Token(TokenType.DEFAULT, '%{name}-%{version}%[%{rc}?"-rc":""]'),
259+
],
260+
),
251261
],
252262
)
253263
def test_options_tokenize(option_string, result):

0 commit comments

Comments
 (0)