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) ... print(patch.key, patch.value)
""" """
_ESCAPED_CHARACTERS = {
"\\$": "$",
"\\`": "`",
"\\\"": "\"",
"\\\n": "",
}
def __init__(self, stream: IO[bytes]) -> None: def __init__(self, stream: IO[bytes]) -> None:
""" """
Args: Args:
@@ -121,7 +128,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
self._content = stream.read() self._content = stream.read()
@staticmethod @staticmethod
def _text(node: Node) -> str: def text(node: Node) -> str:
""" """
read the original source text represented by a syntax tree node 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") return cast(bytes, node.text).decode("utf8", errors="backslashreplace")
@staticmethod @staticmethod
def _unquote(value: str) -> str: def unquote(value: str) -> str:
""" """
remove shell quotes and escapes from a single value 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)) 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: def _assignment(self, node: Node) -> PkgbuildPatch:
""" """
convert an assignment node convert an assignment node
@@ -156,10 +194,8 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
Returns: Returns:
PkgbuildPatch: parsed assignment PkgbuildPatch: parsed assignment
""" """
name = cast(Node, node.child_by_field_name(PkgbuildNodeField.Name)) key = self.text(cast(Node, node.child_by_field_name(PkgbuildNodeField.Name)))
value = cast(Node, node.child_by_field_name(PkgbuildNodeField.Value)) value = node.child_by_field_name(PkgbuildNodeField.Value)
key = self._text(name)
# bash permits assignments without a value (for example ``IFS=``). # bash permits assignments without a value (for example ``IFS=``).
# tree-sitter represents those assignments without a value node. # tree-sitter represents those assignments without a value node.
if value is None: if value is None:
@@ -173,7 +209,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
return PkgbuildPatch(key, values) return PkgbuildPatch(key, values)
case _: case _:
return PkgbuildPatch(key, self._value(value)) return PkgbuildPatch(key, self.value(value))
def _expand_array(self, node: Node) -> list[str]: def _expand_array(self, node: Node) -> list[str]:
""" """
@@ -186,7 +222,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
list[str]: expanded element values list[str]: expanded element values
""" """
if node.type != PkgbuildNodeType.Concatenation: if node.type != PkgbuildNodeType.Concatenation:
return [self._value(node)] return [self.value(node)]
values = [""] values = [""]
children = node.named_children children = node.named_children
@@ -196,15 +232,15 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
Node(type=PkgbuildNodeType.Word, text=b"{"), Node(type=PkgbuildNodeType.Word, text=b"{"),
Node(type=PkgbuildNodeType.Word) as content, Node(type=PkgbuildNodeType.Word) as content,
Node(type=PkgbuildNodeType.Word, text=b"}"), 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 = [ parts = [
self._unquote(part).replace(ShellInterpolator.DATA_LINK_ESCAPE, ",") self.unquote(part).replace(ShellInterpolator.DATA_LINK_ESCAPE, ",")
for part in escaped.split(",") for part in escaped.split(",")
] ]
children = children[3:] children = children[3:]
case _: case _:
parts = [self._value(children.pop(0))] parts = [self.value(children.pop(0))]
values = [prefix + part for prefix in values for part in parts] values = [prefix + part for prefix in values for part in parts]
@@ -242,7 +278,7 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
case PkgbuildNodeType.FunctionDefinition: case PkgbuildNodeType.FunctionDefinition:
name = cast(Node, node.child_by_field_name(PkgbuildNodeField.Name)) name = cast(Node, node.child_by_field_name(PkgbuildNodeField.Name))
body = cast(Node, node.child_by_field_name(PkgbuildNodeField.Body)) 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 _: case _:
# descend into statements, but skip command arguments and values # descend into statements, but skip command arguments and values
@@ -256,30 +292,6 @@ class PkgbuildParser(Iterable[PkgbuildPatch]):
): ):
yield from self._read(child, values) 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]: def parse(self) -> Iterator[PkgbuildPatch]:
""" """
parse source and yield extracted entries 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)" _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)" 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"),
] ]
+12
View File
@@ -113,3 +113,15 @@ pkgver() {
_ver="$(cat CMakeLists.txt | grep -e SO_MAJOR_VERSION -e SO_MINOR_VERSION -e SO_BUILD_VERSION | cut -d '"' -f2 | head -n3)" _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)" echo "$(echo ${_ver} | tr ' ' .).r$(git rev-list --count HEAD).$(git rev-parse --short HEAD)"
} }
# escapes and newlines inside double-quoted assignments
var="say \"hello\""
var="a\\b"
var="\$name"
var="\`date\`"
var="first\
second"
var="a\qb"
var="\\\\$name"
var="first
second"