diff --git a/src/sipmessage/address.py b/src/sipmessage/address.py index d22b3b1..5dfd253 100644 --- a/src/sipmessage/address.py +++ b/src/sipmessage/address.py @@ -6,7 +6,7 @@ import dataclasses import re -from . import grammar +from . import grammar, utils from .parameters import Parameters from .uri import URI @@ -22,7 +22,6 @@ "<(?P[^>]+)>" # *(SEMI contact-params) f"(?P(?:{grammar.SEMI}{grammar.GENERIC_PARAM})*)" - "$" ), # addr-spec *(SEMI contact-params) re.compile( @@ -32,7 +31,6 @@ "(?P[^ ;]+)" # *(SEMI contact-params) f"(?P(?:{grammar.SEMI}{grammar.GENERIC_PARAM})*)" - "$" ), ] @@ -70,15 +68,29 @@ def parse(cls, value: str) -> "Address": If parsing fails, a :class:`ValueError` is raised. """ - value = grammar.simplify_whitespace(value) + return utils.parse_single(cls._parse_one, value) + @classmethod + def parse_many(cls, value: str) -> "list[Address]": + """ + Parse the given string into a list of :class:`Address` instances. + + If parsing fails, a :class:`ValueError` is raised. + """ + return utils.parse_many(cls._parse_one, value) + + @classmethod + def _parse_one(cls, value: str) -> "tuple[Address, str]": for pattern in ADDRESS_PATTERNS: m = pattern.match(value) if m: - return cls( - uri=URI.parse(m.group("uri")), - name=unquote(m.group("name").strip()), - parameters=Parameters.parse(m.group("parameters")), + return ( + cls( + uri=URI.parse(m.group("uri")), + name=unquote(m.group("name").strip()), + parameters=Parameters.parse(m.group("parameters")), + ), + value[m.end() :], ) else: raise ValueError("Address is not valid") diff --git a/src/sipmessage/utils.py b/src/sipmessage/utils.py new file mode 100644 index 0000000..79c388c --- /dev/null +++ b/src/sipmessage/utils.py @@ -0,0 +1,30 @@ +import typing + +from . import grammar + +T = typing.TypeVar("T") + + +def parse_many(parser: typing.Callable[[str], tuple[T, str]], value: str) -> list[T]: + value = grammar.simplify_whitespace(value) + + items: list[T] = [] + while value: + item, value = parser(value) + items.append(item) + + # Consume list separator(s). + while value.startswith(","): + value = value[1:].lstrip() + + return items + + +def parse_single(parser: typing.Callable[[str], tuple[T, str]], value: str) -> T: + value = grammar.simplify_whitespace(value) + + item, value = parser(value) + if value: + raise ValueError("Header has trailing data") + + return item diff --git a/src/sipmessage/via.py b/src/sipmessage/via.py index 692b3da..4138a83 100644 --- a/src/sipmessage/via.py +++ b/src/sipmessage/via.py @@ -6,7 +6,7 @@ import dataclasses import re -from . import grammar +from . import grammar, utils from .parameters import Parameters VIA_PATTERN = re.compile( @@ -14,7 +14,6 @@ f"(?P{grammar.HOST})" f"(?::(?P{grammar.PORT}))?" f"(?P(?:{grammar.SEMI}{grammar.GENERIC_PARAM})*)" - "$" ) @@ -43,8 +42,19 @@ def parse(cls, value: str) -> "Via": If parsing fails, a :class:`ValueError` is raised. """ - value = grammar.simplify_whitespace(value) + return utils.parse_single(cls._parse_one, value) + @classmethod + def parse_many(cls, value: str) -> "list[Via]": + """ + Parse the given string into a list of :class:`Via` instances. + + If parsing fails, a :class:`ValueError` is raised. + """ + return utils.parse_many(cls._parse_one, value) + + @classmethod + def _parse_one(cls, value: str) -> tuple["Via", str]: m = VIA_PATTERN.match(value) if m is None: raise ValueError("Via is not valid") @@ -56,7 +66,7 @@ def parse(cls, value: str) -> "Via": host=m.group("host"), port=int(port) if port else None, parameters=Parameters.parse(m.group("parameters")), - ) + ), value[m.end() :] def __str__(self) -> str: s = f"SIP/2.0/{self.transport} {self.host}" diff --git a/tests/test_address.py b/tests/test_address.py index 66916c7..f10b7a6 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -200,3 +200,23 @@ def test_rfc4475_intmeth_to(self) -> None: '"BEL:\x07 NUL:\x00 DEL:\x7f" ' "", ) + + def test_trailing_comma(self) -> None: + with self.assertRaises(ValueError) as cm: + Address.parse(",") + self.assertEqual(str(cm.exception), "Header has trailing data") + + +class AddressParseManyTest(unittest.TestCase): + def test_simple(self) -> None: + contacts = Address.parse_many( + ", , " + ) + self.assertEqual( + contacts, + [ + Address(uri=URI(scheme="sip", host="atlanta.com", user="alice")), + Address(uri=URI(scheme="sip", host="biloxi.com", user="bob")), + Address(uri=URI(scheme="sip", host="chicago.com", user="carol")), + ], + ) diff --git a/tests/test_via.py b/tests/test_via.py index 697ae89..8666ace 100644 --- a/tests/test_via.py +++ b/tests/test_via.py @@ -116,3 +116,26 @@ def test_rfc4475_wsinv(self) -> None: parameters=Parameters(branch="z9hG4bK30239"), ), ) + + +class ViaParseManyTest(unittest.TestCase): + def test_simple(self) -> None: + vias = Via.parse_many( + "SIP/2.0/UDP 192.168.255.111;branch=z9hG4bK30239, " + "SIP/2.0/WSS T8trJdbBz7r6.invalid;branch=z9hG4bKYJHC9fb" + ) + self.assertEqual( + vias, + [ + Via( + transport="UDP", + host="192.168.255.111", + parameters=Parameters(branch="z9hG4bK30239"), + ), + Via( + transport="WSS", + host="T8trJdbBz7r6.invalid", + parameters=Parameters(branch="z9hG4bKYJHC9fb"), + ), + ], + )