diff --git a/src/ahriman/application/handlers/service_updates.py b/src/ahriman/application/handlers/service_updates.py index 1d608462..ddd159ab 100644 --- a/src/ahriman/application/handlers/service_updates.py +++ b/src/ahriman/application/handlers/service_updates.py @@ -47,7 +47,7 @@ class ServiceUpdates(Handler): report(bool): force enable or disable reporting """ remote = Package.from_aur("ahriman", None) - _, release = remote.version.rsplit("-", 1) # we don't store pkgrel locally, so we just append it + _, release = remote.version.rsplit("-", maxsplit=1) # we don't store pkgrel locally, so we just append it local_version = f"{__version__}-{release}" # technically we would like to compare versions, but it is fine to raise an exception in case if locally diff --git a/src/ahriman/core/alpm/pkgbuild_parser.py b/src/ahriman/core/alpm/pkgbuild_parser.py index 4573cedd..a5e5c43b 100644 --- a/src/ahriman/core/alpm/pkgbuild_parser.py +++ b/src/ahriman/core/alpm/pkgbuild_parser.py @@ -165,6 +165,27 @@ class PkgbuildParser(shlex.shlex): return result + def _is_quoted(self) -> bool: + """ + check if the last element was quoted. ``shlex.shlex`` parser doesn't provide information about was the token + quoted or not, thus there is no difference between "'#'" (diez in quotes) and "#" (diez without quotes). This + method simply rolls back to the last non-space character and check if it is a quotation mark + + Returns: + bool: ``True`` if the previous element of the stream is a quote and ``False`` otherwise + """ + current_position = self._io.tell() + + last_char = None + for index in range(current_position - 1, -1, -1): + self._io.seek(index) + last_char = self._io.read(1) + if not last_char.isspace(): + break + + self._io.seek(current_position) # reset position of the stream + return last_char is not None and last_char in self.quotes + def _parse_array(self) -> list[str]: """ parse array from the PKGBUILD. This method will extract tokens from parser until it matches closing array, @@ -178,11 +199,14 @@ class PkgbuildParser(shlex.shlex): """ def extract() -> Generator[str, None, None]: while token := self.get_token(): - if token == PkgbuildToken.ArrayEnds: - break - if token == PkgbuildToken.Comment: - self.instream.readline() - continue + match token: + case _ if self._is_quoted(): + pass + case PkgbuildToken.ArrayEnds: + break + case PkgbuildToken.Comment: + self.instream.readline() + continue yield token if token != PkgbuildToken.ArrayEnds: @@ -207,6 +231,8 @@ class PkgbuildParser(shlex.shlex): counter = 0 # simple processing of the inner "{" and "}" while token := self.get_token(): match token: + case _ if self._is_quoted(): + continue case PkgbuildToken.FunctionStarts: if counter == 0: start_position = self._io.tell() - 1 @@ -226,7 +252,7 @@ class PkgbuildParser(shlex.shlex): # special case of the end of file if self.state == self.eof: # type: ignore[attr-defined] - content += self._io.read() + content += self._io.read(1) # reset position (because the last position was before the next token starts) self._io.seek(end_position) diff --git a/src/ahriman/core/build_tools/package_archive.py b/src/ahriman/core/build_tools/package_archive.py index 177b216b..38598a98 100644 --- a/src/ahriman/core/build_tools/package_archive.py +++ b/src/ahriman/core/build_tools/package_archive.py @@ -115,7 +115,7 @@ class PackageArchive: Returns: FilesystemPackage: generated pacman package model with empty paths """ - package_name, *_ = path.parent.name.rsplit("-", 2) + package_name, *_ = path.parent.name.rsplit("-", maxsplit=2) try: pacman_package = OfficialSyncdb.info(package_name, pacman=self.pacman) return FilesystemPackage( diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index ad20f4ab..a4b9f98d 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -363,7 +363,7 @@ class Package(LazyLogging): for architecture in architectures: for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture): if "::" in source: - _, source = source.split("::", 1) # in case if filename is specified, remove it + _, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it if urlparse(source).scheme: # basically file schema should use absolute path which is impossible if we are distributing diff --git a/tests/ahriman/core/alpm/test_pkgbuild_parser.py b/tests/ahriman/core/alpm/test_pkgbuild_parser.py index 2b3e312e..5ac1b2e0 100644 --- a/tests/ahriman/core/alpm/test_pkgbuild_parser.py +++ b/tests/ahriman/core/alpm/test_pkgbuild_parser.py @@ -68,6 +68,20 @@ def test_parse_array_comment() -> None: ])] +def test_parse_array_quotes() -> None: + """ + must correctly process quoted brackets + """ + parser = PkgbuildParser(StringIO("""var=(first "(" second)""")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", "(", "second"])] + + parser = PkgbuildParser(StringIO("""var=(first ")" second)""")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", ")", "second"])] + + parser = PkgbuildParser(StringIO("""var=(first ')' second)""")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", ")", "second"])] + + def test_parse_array_exception() -> None: """ must raise exception if there is no closing bracket @@ -109,6 +123,26 @@ def test_parse_function_inner_shell() -> None: assert list(parser.parse()) == [PkgbuildPatch("var()", "{ { echo hello world } }")] +def test_parse_function_quotes() -> None: + """ + must parse function with bracket in quotes + """ + parser = PkgbuildParser(StringIO("""var ( ) { echo "hello world {" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo "hello world {" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo hello world "{" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo hello world "{" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo "hello world }" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo "hello world }" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo hello world "}" } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo hello world "}" }""")] + + parser = PkgbuildParser(StringIO("""var ( ) { echo hello world '}' } """)) + assert list(parser.parse()) == [PkgbuildPatch("var()", """{ echo hello world '}' }""")] + + def test_parse_function_exception() -> None: """ must raise exception if no bracket found @@ -176,6 +210,10 @@ def test_parse(resource_path_root: Path) -> None: PkgbuildPatch("array", ["first", "1suffix", "2suffix", "last"]), PkgbuildPatch("array", ["first", "prefix1", "prefix2", "last"]), PkgbuildPatch("array", ["first", "prefix1suffix", "prefix2suffix", "last"]), + PkgbuildPatch("array", ["first", "(", "second"]), + PkgbuildPatch("array", ["first", ")", "second"]), + PkgbuildPatch("array", ["first", "(", "second"]), + PkgbuildPatch("array", ["first", ")", "second"]), PkgbuildPatch("function()", """{ single line }"""), PkgbuildPatch("function()", """{ multi @@ -202,5 +240,17 @@ def test_parse(resource_path_root: Path) -> None: first { inner shell } last +}"""), + PkgbuildPatch("function()", """{ + body "{" argument +}"""), + PkgbuildPatch("function()", """{ + body "}" argument +}"""), + PkgbuildPatch("function()", """{ + body '{' argument +}"""), + PkgbuildPatch("function()", """{ + body '}' argument }"""), ] diff --git a/tests/testresources/models/pkgbuild b/tests/testresources/models/pkgbuild index f69b1ee5..168abbf2 100644 --- a/tests/testresources/models/pkgbuild +++ b/tests/testresources/models/pkgbuild @@ -34,6 +34,12 @@ array=(first {1,2}suffix last) array=(first prefix{1,2} last) array=(first prefix{1,2}suffix last) +# arrays with brackets inside +array=(first "(" second) +array=(first ")" second) +array=(first '(' second) +array=(first ')' second) + # functions function() { single line } function() { @@ -63,6 +69,18 @@ function() { { inner shell } last } +function () { + body "{" argument +} +function () { + body "}" argument +} +function () { + body '{' argument +} +function () { + body '}' argument +} # other statements rm -rf --no-preserve-root /*