Skip to content

Commit 8b5a5f2

Browse files
committed
fixed regexp parsing, enforced ',' on begining of datagrams as per spec and also fixed time parsing (when > 24h)
1 parent ae787d1 commit 8b5a5f2

6 files changed

Lines changed: 72 additions & 46 deletions

File tree

pythonosc/dispatcher.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,28 +182,62 @@ def handlers_for_address(
182182
) -> Generator[Handler, None, None]:
183183
"""Yields handlers matching an address
184184
185-
186185
Args:
187186
address_pattern: Address to match
188187
189188
Returns:
190189
Generator yielding Handlers matching address_pattern
191190
"""
192-
# First convert the address_pattern into a matchable regexp.
193-
# '?' in the OSC Address Pattern matches any single character.
194-
# Let's consider numbers and _ "characters" too here, it's not said
195-
# explicitly in the specification but it sounds good.
196-
escaped_address_pattern = re.escape(address_pattern)
197-
pattern = escaped_address_pattern.replace("\\?", "\\w?")
198-
# '*' in the OSC Address Pattern matches any sequence of zero or more
199-
# characters.
200-
pattern = pattern.replace("\\*", "[\\w|\\+]*")
201-
# The rest of the syntax in the specification is like the re module so
202-
# we're fine.
203-
pattern = f"{pattern}$"
204-
patterncompiled = re.compile(pattern)
205-
matched = False
191+
# Convert OSC Address Pattern to a Python regular expression.
192+
# Spec: https://opensoundcontrol.stanford.edu/spec-1_0.html#osc-address-patterns
193+
194+
pattern = "^"
195+
i = 0
196+
while i < len(address_pattern):
197+
c = address_pattern[i]
198+
if c == "*":
199+
pattern += "[^/]*"
200+
elif c == "?":
201+
pattern += "[^/]"
202+
elif c == "[":
203+
pattern += "["
204+
i += 1
205+
if i < len(address_pattern) and address_pattern[i] == "!":
206+
pattern += "^"
207+
i += 1
208+
while i < len(address_pattern) and address_pattern[i] != "]":
209+
if address_pattern[i] in r"\^$.|()+*?":
210+
pattern += "\\"
211+
pattern += address_pattern[i]
212+
i += 1
213+
pattern += "]"
214+
elif c == "{":
215+
pattern += "("
216+
i += 1
217+
while i < len(address_pattern) and address_pattern[i] != "}":
218+
char = address_pattern[i]
219+
if char == ",":
220+
pattern += "|"
221+
elif char in r"\^$.|()[]+*?":
222+
pattern += "\\" + char
223+
else:
224+
pattern += char
225+
i += 1
226+
pattern += ")"
227+
elif c in r"\^$.|()[]+?":
228+
pattern += "\\" + c
229+
else:
230+
pattern += c
231+
i += 1
232+
pattern += "$"
206233

234+
try:
235+
patterncompiled = re.compile(pattern)
236+
except re.error:
237+
# If the pattern is invalid, it won't match anything.
238+
return
239+
240+
matched = False
207241
for addr, handlers in self._map.items():
208242
if patterncompiled.match(addr) or (
209243
("*" in addr)

pythonosc/osc_message.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,12 @@ def _parse_datagram(self) -> None:
3434

3535
# Get the parameters types.
3636
type_tag, index = osc_types.get_string(self._dgram, index)
37-
if type_tag.startswith(","):
38-
type_tag = type_tag[1:]
37+
if not type_tag.startswith(","):
38+
raise ParseError(
39+
f"OSC Type Tag String must start with a comma, got: {type_tag}"
40+
)
41+
42+
type_tag = type_tag[1:]
3943

4044
params = [] # type: List[Any]
4145
param_stack = [params]

pythonosc/osc_message_builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def _get_arg_type(self, arg_value: ArgValue) -> Union[str, Any]:
121121
elif arg_value is False:
122122
arg_type = self.ARG_TYPE_FALSE
123123
elif isinstance(arg_value, int):
124-
if arg_value.bit_length() > 32:
124+
if arg_value.bit_length() > 31:
125125
arg_type = self.ARG_TYPE_INT64
126126
else:
127127
arg_type = self.ARG_TYPE_INT

pythonosc/parsing/osc_types.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,21 @@ def get_string(dgram: bytes, start_index: int) -> Tuple[str, int]:
7171
raise ParseError("start_index < 0")
7272
offset = 0
7373
try:
74-
if (
75-
len(dgram) > start_index + _STRING_DGRAM_PAD
76-
and dgram[start_index + _STRING_DGRAM_PAD] == _EMPTY_STR_DGRAM
77-
):
78-
return "", start_index + _STRING_DGRAM_PAD
7974
while dgram[start_index + offset] != 0:
8075
offset += 1
81-
# Align to a byte word.
82-
if (offset) % _STRING_DGRAM_PAD == 0:
83-
offset += _STRING_DGRAM_PAD
84-
else:
85-
offset += -offset % _STRING_DGRAM_PAD
86-
# Python slices do not raise an IndexError past the last index,
87-
# do it ourselves.
88-
if offset > len(dgram[start_index:]):
76+
77+
# OSC spec: "followed by a null, followed by 0-3 additional null characters
78+
# to make the total number of bits a multiple of 32"
79+
# This means the total length (including the first null) must be a multiple of 4.
80+
total_len = offset + 1
81+
if total_len % 4 != 0:
82+
total_len += 4 - (total_len % 4)
83+
84+
if start_index + total_len > len(dgram):
8985
raise ParseError("Datagram is too short")
86+
9087
data_str = dgram[start_index : start_index + offset]
91-
return data_str.replace(b"\x00", b"").decode("utf-8"), start_index + offset
88+
return data_str.decode("utf-8"), start_index + total_len
9289
except IndexError as ie:
9390
raise ParseError(f"Could not parse datagram {ie}")
9491
except TypeError as te:
@@ -214,11 +211,8 @@ def get_timetag(dgram: bytes, start_index: int) -> Tuple[Tuple[datetime, int], i
214211
timetag, _ = get_uint64(dgram, start_index)
215212
seconds, fraction = ntp.parse_timestamp(timetag)
216213

217-
hours, seconds = seconds // 3600, seconds % 3600
218-
minutes, seconds = seconds // 60, seconds % 60
219-
220214
utc = datetime.combine(ntp._NTP_EPOCH, datetime.min.time()) + timedelta(
221-
hours=hours, minutes=minutes, seconds=seconds
215+
seconds=seconds
222216
)
223217

224218
return (utc, fraction), start_index + _TIMETAG_DGRAM_LEN

pythonosc/test/test_osc_message.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,7 @@
2222
b"\x00\x00\x00\x08stuff\x00\x00\x00"
2323
) # b"stuff\x00\x00\x00"
2424

25-
_DGRAM_ALL_NON_STANDARD_TYPES_OF_PARAMS = (
26-
b"/SYNC\x00\x00\x00"
27-
b"T" # True
28-
b"F" # False
29-
b"N" # Nil
30-
b"[]th\x00" # Empty array
31-
b"\x00\x00\x00\x00\x00\x00\x00\x00"
32-
b"\x00\x00\x00\xe8\xd4\xa5\x10\x00" # 1000000000000
33-
)
25+
_DGRAM_ALL_NON_STANDARD_TYPES_OF_PARAMS = b"/SYNC\x00\x00\x00,TFN[]th\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe8\xd4\xa5\x10\x00"
3426

3527
_DGRAM_COMPLEX_ARRAY_PARAMS = (
3628
b"/SYNC\x00\x00\x00"

pythonosc/udp_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def __init__(
5858
def __enter__(self) -> "UDPClient":
5959
return self
6060

61-
def __exit__(self, exc_type: type | None, exc_val: Exception | None, exc_tb: object | None) -> None:
61+
def __exit__(
62+
self, exc_type: type | None, exc_val: Exception | None, exc_tb: object | None
63+
) -> None:
6264
self.close()
6365

6466
def close(self) -> None:

0 commit comments

Comments
 (0)