fix: improve parsing of escaped symbols inside string

This commit is contained in:
arcanis committed 2026-10-30 16:16:50 +02:00
1 parent c56a319267
commit 5ad8a2a7d0
3 files changed
+68 -36

No files matched your search

@@ -113,6 +113,13 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
... print(patch.key, patch.value)
"""
_ESCAPED_CHARACTERS = {
"\\$": "$",
"\\`": "`",
"\\\"": "\"",
"\\\n": "",
}
def __init__(self, stream: IO[bytes]) -> None:
"""
Args:
@@ -121,7 +128,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
self._content = stream.read()
@staticmethod
def _text(node: Node) -> str:
def text(node: Node) -> str:
"""
read the original source text represented by a syntax tree node
@@ -134,7 +141,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
return cast(bytes, node.text).decode("utf8", errors="backslashreplace")
@staticmethod
def _unquote(value: str) -> str:
def unquote(value: str) -> str:
"""
remove shell quotes and escapes from a single value
@@ -146,6 +153,37 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
"""
return "".join(shlex.split(value, comments=False, posix=True))
@staticmethod
def value(node: Node) -> str:
"""
extract a string value from a syntax tree node
Args:
node(Node): syntax tree node to extract
Returns:
str: extracted string value
"""
def unescape(source: str) -> str:
for original, replacement in PkgbuildParser._ESCAPED_CHARACTERS.items():
source = source.replace(original, replacement)
return source
if node.type == PkgbuildNodeType.Concatenation:
return "".join(PkgbuildParser.value(child) for child in node.named_children)
value = PkgbuildParser.text(node)
match node.type:
case PkgbuildNodeType.String | PkgbuildNodeType.StringContent:
if node.type == PkgbuildNodeType.String:
value = PkgbuildParser.unquote(value)
# protect escaped backslashes from being interpreted a second time.
return "\\".join(unescape(part) for part in value.split("\\\\"))
case PkgbuildNodeType.Number | PkgbuildNodeType.RawString | PkgbuildNodeType.Word:
return PkgbuildParser.unquote(value)
case _:
return value
def _assignment(self, node: Node) -> PkgbuildPatch:
"""
convert an assignment node
@@ -156,10 +194,8 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
Returns:
PkgbuildPatch: parsed assignment
"""
name = cast(Node, node.child_by_field_name(PkgbuildNodeField.Name))
value = cast(Node, node.child_by_field_name(PkgbuildNodeField.Value))
key = self._text(name)
key = self.text(cast(Node, node.child_by_field_name(PkgbuildNodeField.Name)))
value = node.child_by_field_name(PkgbuildNodeField.Value)
# bash permits assignments without a value (for example ``IFS=``).
# tree-sitter represents those assignments without a value node.
if value is None:
@@ -173,7 +209,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
return PkgbuildPatch(key, values)
case _:
return PkgbuildPatch(key, self._value(value))
return PkgbuildPatch(key, self.value(value))
def _expand_array(self, node: Node) -> list[str]:
"""
@@ -186,7 +222,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
list[str]: expanded element values
"""
if node.type != PkgbuildNodeType.Concatenation:
return [self._value(node)]
return [self.value(node)]
values = [""]
children = node.named_children
@@ -196,15 +232,15 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
Node(type=PkgbuildNodeType.Word, text=b"{"),
Node(type=PkgbuildNodeType.Word) as content,
Node(type=PkgbuildNodeType.Word, text=b"}"),
] if "," in (escaped := self._text(content).replace("\\,", ShellInterpolator.DATA_LINK_ESCAPE)):
] if "," in (escaped := self.text(content).replace("\\,", ShellInterpolator.DATA_LINK_ESCAPE)):
parts = [
self._unquote(part).replace(ShellInterpolator.DATA_LINK_ESCAPE, ",")
self.unquote(part).replace(ShellInterpolator.DATA_LINK_ESCAPE, ",")
for part in escaped.split(",")
]
children = children[3:]
case _:
parts = [self._value(children.pop(0))]
parts = [self.value(children.pop(0))]
values = [prefix + part for prefix in values for part in parts]
@@ -242,7 +278,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
case PkgbuildNodeType.FunctionDefinition:
name = cast(Node, node.child_by_field_name(PkgbuildNodeField.Name))
body = cast(Node, node.child_by_field_name(PkgbuildNodeField.Body))
yield PkgbuildPatch(f"{self._text(name)}()", self._text(body))
yield PkgbuildPatch(f"{self.text(name)}()", self.text(body))
case _:
# descend into statements, but skip command arguments and values
@@ -256,30 +292,6 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
):
yield from self._read(child, values)
def _value(self, node: Node) -> str:
"""
extract a string value from a syntax tree node
Args:
node(Node): syntax tree node to extract
Returns:
str: extracted string value
"""
if node.type in (PkgbuildNodeType.Concatenation, PkgbuildNodeType.String):
return "".join(self._value(child) for child in node.named_children)
value = self._text(node)
match node.type:
case PkgbuildNodeType.StringContent:
value = f'"{value}"'
case PkgbuildNodeType.Number | PkgbuildNodeType.RawString | PkgbuildNodeType.Word:
pass
case _:
return value
return self._unquote(value)
def parse(self) -> Iterator[PkgbuildPatch]:
"""
parse source and yield extracted entries
@@ -282,4 +282,12 @@ def test_parse(resource_path_root: Path) -> None:
_ver="$(cat CMakeLists.txt | grep -e SO_MAJOR_VERSION -e SO_MINOR_VERSION -e SO_BUILD_VERSION | cut -d '"' -f2 | head -n3)"
echo "$(echo ${_ver} | tr ' ' .).r$(git rev-list --count HEAD).$(git rev-parse --short HEAD)"
}"""),
PkgbuildPatch("var", "say \"hello\""),
PkgbuildPatch("var", "a\\b"),
PkgbuildPatch("var", "$name"),
PkgbuildPatch("var", "`date`"),
PkgbuildPatch("var", "firstsecond"),
PkgbuildPatch("var", "a\\qb"),
PkgbuildPatch("var", "\\$name"),
PkgbuildPatch("var", "first\nsecond"),
]