Skip to content

Commit

Permalink
Make it possible to parse multiple Address and Via from a string
Browse files Browse the repository at this point in the history
  • Loading branch information
jlaine committed Jun 9, 2024
1 parent f09c223 commit 6cac319
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 12 deletions.
28 changes: 20 additions & 8 deletions src/sipmessage/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import dataclasses
import re

from . import grammar
from . import grammar, utils
from .parameters import Parameters
from .uri import URI

Expand All @@ -22,7 +22,6 @@
"<(?P<uri>[^>]+)>"
# *(SEMI contact-params)
f"(?P<parameters>(?:{grammar.SEMI}{grammar.GENERIC_PARAM})*)"
"$"
),
# addr-spec *(SEMI contact-params)
re.compile(
Expand All @@ -32,7 +31,6 @@
"(?P<uri>[^ ;]+)"
# *(SEMI contact-params)
f"(?P<parameters>(?:{grammar.SEMI}{grammar.GENERIC_PARAM})*)"
"$"
),
]

Expand Down Expand Up @@ -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")
Expand Down
30 changes: 30 additions & 0 deletions src/sipmessage/utils.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 14 additions & 4 deletions src/sipmessage/via.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
import dataclasses
import re

from . import grammar
from . import grammar, utils
from .parameters import Parameters

VIA_PATTERN = re.compile(
f"^SIP{grammar.SLASH}2\\.0{grammar.SLASH}(?P<transport>{grammar.TOKEN}) "
f"(?P<host>{grammar.HOST})"
f"(?::(?P<port>{grammar.PORT}))?"
f"(?P<parameters>(?:{grammar.SEMI}{grammar.GENERIC_PARAM})*)"
"$"
)


Expand Down Expand Up @@ -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")
Expand All @@ -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}"
Expand Down
20 changes: 20 additions & 0 deletions tests/test_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,23 @@ def test_rfc4475_intmeth_to(self) -> None:
'"BEL:\x07 NUL:\x00 DEL:\x7f" '
"<sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*@example.com>",
)

def test_trailing_comma(self) -> None:
with self.assertRaises(ValueError) as cm:
Address.parse("<sip:1.2.3.4;lr>,")
self.assertEqual(str(cm.exception), "Header has trailing data")


class AddressParseManyTest(unittest.TestCase):
def test_simple(self) -> None:
contacts = Address.parse_many(
"<sip:[email protected]>, <sip:[email protected]>, <sip:[email protected]>"
)
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")),
],
)
23 changes: 23 additions & 0 deletions tests/test_via.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
],
)

0 comments on commit 6cac319

Please sign in to comment.