python-osc 1.10.2__py3-none-any.whl → 1.10.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-osc
3
- Version: 1.10.2
3
+ Version: 1.10.3
4
4
  Summary: Open Sound Control server and client implementations in pure Python
5
5
  Keywords: osc,sound,midi,music
6
6
  Author: attwad
@@ -0,0 +1,36 @@
1
+ pythonosc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pythonosc/dispatcher.py,sha256=L1d_jm33uwsSHrNlfaLhTXflc70apcK5FwRTJTuEf3Q,15051
3
+ pythonosc/osc_bundle.py,sha256=k8CR9K-MFr8iKj3pYSj72PE2nyhZsIle-GnVpqSrcvM,4151
4
+ pythonosc/osc_bundle_builder.py,sha256=1m1K1bC3NIbUNjNo8lRFQ-Hciyq4hzWEPQm48c1W8DA,2113
5
+ pythonosc/osc_message.py,sha256=GlWwFMhquvt6JuKo8hC4q_S9aSHYTGsfTUUPvO-5uaQ,4797
6
+ pythonosc/osc_message_builder.py,sha256=CHgpPdU9mfTZ52Dt10J0nrvG85OzP474lAhzKGSU_dE,8080
7
+ pythonosc/osc_packet.py,sha256=72LVRxaPaTwkkih7tsPWgWJlyeHxO7xt3SOOK8YMh50,2778
8
+ pythonosc/osc_server.py,sha256=j-cwrZYeiZJTrbYXaW01JiH0H0IFRQLqXrfv0BsdxTY,7445
9
+ pythonosc/osc_tcp_server.py,sha256=Bh_WJHjrtWMtU1gOpY7X1jZEpZNJN5hztllx_UoHT1M,7141
10
+ pythonosc/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pythonosc/parsing/framing.py,sha256=P31sXt9_mO9bUUNjd6bBlS7T3EGUEh129PZ93QzeOhs,4492
12
+ pythonosc/parsing/ntp.py,sha256=9lpFonFynAVYtW5J2ttgnbjM0ej8mTl0BFsm82Ud8Ek,1877
13
+ pythonosc/parsing/osc_types.py,sha256=-YfmtM0q-WVliJwXJaYEcY5MfGYzp0xyTu2eZ6z9GSE,15008
14
+ pythonosc/parsing/slip.py,sha256=KhEBQ3v21A3liulQWMiBYw3oVY_BtEb1wZj1q3tnrIk,3676
15
+ pythonosc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ pythonosc/tcp_client.py,sha256=ywJaAMWSF_a3kuNAYEe_of-jccwXsnOHM84G7cuHDmc,8039
17
+ pythonosc/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ pythonosc/test/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ pythonosc/test/parsing/test_framing.py,sha256=IYvt_FgLUkeFabbleSuxVqcMiRToy38ugxYaMBTcL9o,5048
20
+ pythonosc/test/parsing/test_ntp.py,sha256=KY4XiArsNZtni46Glq0F6dR3psVDbOROdOjFupTWVuI,605
21
+ pythonosc/test/parsing/test_osc_types.py,sha256=TQz0-yCT7FCN3DjpR3e22tiLpX829rGeogj1tuim-P8,13805
22
+ pythonosc/test/test_dispatcher.py,sha256=C0TsHqoino_2_Dnu10WCBROtB9zi4a2pjft4NvFO7Ps,9367
23
+ pythonosc/test/test_empty_string_messages.py,sha256=F6OpZP5xJP3RY_leWigJd6MdQF6Cdm_v5js3_U8Ba6Q,3768
24
+ pythonosc/test/test_osc_bundle.py,sha256=eXsIl_Jpo9lVmcDVG9o4rWlpTyo7XfBG294_uXp-6Vg,4837
25
+ pythonosc/test/test_osc_bundle_builder.py,sha256=7P2DPBun2TrT_4gUq2_haEJiNC8dbGHwzaRF5MJzJHs,1534
26
+ pythonosc/test/test_osc_message.py,sha256=sDU73FGbEKL5tLYyD04FUsC4aJ1jWxJJmoAaRoWTuuI,13250
27
+ pythonosc/test/test_osc_message_builder.py,sha256=RaFPH9biU6P7LZ-p3N7H5Cfg5w6d2BBxVrwGA9EWwCw,3982
28
+ pythonosc/test/test_osc_packet.py,sha256=GCK7mYKv0bzOwCuhwbA7eaxPOjsn-pTzTbpMzWTXo-s,2163
29
+ pythonosc/test/test_osc_server.py,sha256=MtPJ6ZdEIArRrZxkIPF2_0Dlic0Ct3beQlHZpFgIOaQ,6738
30
+ pythonosc/test/test_osc_tcp_server.py,sha256=Yhj5EGsYMpSwsxuHIMFyi5GEZIpPKxSA-LstWVJXhJs,13972
31
+ pythonosc/test/test_tcp_client.py,sha256=BxRhzaiSoiY5Iz_cYlO45WWKejpdLT7HFciMpu5zXX4,2605
32
+ pythonosc/test/test_udp_client.py,sha256=qLP5SPem6T40JbAoRlf5_N258pTvLzgu3BAqC6PkzMI,3487
33
+ pythonosc/udp_client.py,sha256=4E9GU8dPV3RilCskedzzx2jVcgK6EGp1UXrGjYV3ZaI,5105
34
+ python_osc-1.10.3.dist-info/WHEEL,sha256=3OmCxFA0ffBIsX4x0o8CdfSvZAOvO3mzEG8U_z5TvV4,80
35
+ python_osc-1.10.3.dist-info/METADATA,sha256=vpKkB59PbIqVCpXGHAI3iN0K05Ql7_TMyw6UPLndAAk,6387
36
+ python_osc-1.10.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.11.3
2
+ Generator: uv 0.13.8
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
pythonosc/dispatcher.py CHANGED
@@ -154,13 +154,15 @@ class Dispatcher(object):
154
154
 
155
155
  The callback function must have one of the following signatures:
156
156
 
157
- ``def some_cb(address: str, *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
158
- ``def some_cb(address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr,
159
- Tuple(str, ArgValue)]:``
157
+ .. code-block:: python
160
158
 
161
- ``def some_cb(client_address: Tuple[str, int], address: str, *osc_args: List[Any]) -> Union[None, AnyStr,
162
- Tuple(str, ArgValue)]:``
163
- ``def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
159
+ def some_cb(address: str, *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
160
+ def some_cb(address: str, fixed_args: List[Any], *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
161
+ def some_cb(client_address: Tuple[str, int], address: str, *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
162
+ def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
163
+
164
+ Each variadic OSC argument is a single ArgValue, not a list of all arguments.
165
+ Any fixed arguments passed to map() are collected into the separate fixed_args list.
164
166
 
165
167
  The callback function can return None, or a string representing an OSC address to be returned to the client,
166
168
  or a tuple that includes the address and ArgValue which will be converted to an OSC message and returned to
pythonosc/osc_bundle.py CHANGED
@@ -50,6 +50,12 @@ class OscBundle(object):
50
50
  while self._dgram[index:]:
51
51
  # Get the sub content size.
52
52
  content_size, index = osc_types.get_int(self._dgram, index)
53
+ if content_size <= 0 or content_size % 4:
54
+ raise ParseError(
55
+ "Invalid bundle element size: {}".format(content_size)
56
+ )
57
+ if content_size > len(self._dgram) - index:
58
+ raise ParseError("Bundle element exceeds the datagram length")
53
59
  # Get the datagram for the sub content.
54
60
  content_dgram = self._dgram[index : index + content_size]
55
61
  # Increment our position index up to the next possible content.
@@ -200,13 +200,16 @@ class OscMessageBuilder(object):
200
200
 
201
201
 
202
202
  def build_msg(
203
- address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
203
+ address: str, value: Union[ArgValue, Iterable[ArgValue]] = ()
204
204
  ) -> osc_message.OscMessage:
205
+ """Build a message from a scalar value or an iterable of arguments.
206
+
207
+ Omit the value or pass an empty iterable for a message with no arguments.
208
+ An explicitly supplied empty string is retained as a string argument.
209
+ """
205
210
  builder = OscMessageBuilder(address=address)
206
211
  values: Iterable[Any]
207
- if value == "":
208
- values = []
209
- elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
212
+ if not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
210
213
  values = [value]
211
214
  else:
212
215
  values = value
pythonosc/osc_server.py CHANGED
@@ -59,6 +59,9 @@ def _is_valid_request(request: _RequestType) -> bool:
59
59
  class OSCUDPServer(socketserver.UDPServer):
60
60
  """Superclass for different flavors of OSC UDP servers"""
61
61
 
62
+ # Receive complete UDP datagrams instead of socketserver's 8192-byte default.
63
+ max_packet_size = 65535
64
+
62
65
  def __init__(
63
66
  self,
64
67
  server_address: Tuple[str, int],
@@ -33,68 +33,37 @@ loop.run_forever()
33
33
  # mypy: disable-error-code="attr-defined"
34
34
 
35
35
  import asyncio
36
- import logging
37
36
  import os
38
37
  import socket
39
38
  import socketserver
40
- import struct
41
- from typing import List, Tuple
39
+ from typing import Tuple
42
40
 
43
- from pythonosc import osc_message_builder, slip
41
+ from pythonosc import osc_message_builder
44
42
  from pythonosc.dispatcher import Dispatcher
43
+ from pythonosc.parsing.framing import (
44
+ BINARY_LENGTH_FRAMING,
45
+ SLIP_FRAMING,
46
+ Framing,
47
+ FramingParser,
48
+ )
45
49
 
46
- LOG = logging.getLogger()
47
50
  MODE_1_0 = "1.0"
48
51
  MODE_1_1 = "1.1"
52
+ FRAMING_BY_MODE: dict[str, Framing] = {
53
+ MODE_1_0: BINARY_LENGTH_FRAMING,
54
+ MODE_1_1: SLIP_FRAMING,
55
+ }
49
56
 
50
57
 
51
- class _TCPHandler1_0(socketserver.BaseRequestHandler):
52
- """Handles correct OSC1.0 messages.
58
+ def get_framing(mode: str) -> Framing:
59
+ try:
60
+ return FRAMING_BY_MODE[mode]
61
+ except KeyError:
62
+ raise ValueError(f"Unsupported OSC mode: {mode}") from None
53
63
 
54
- Whether this will be run on its own thread, the server's or a whole new
55
- process depends on the server you instantiated, look at their documentation.
56
64
 
57
- This method is called after a basic sanity check was done on the datagram,
58
- basically whether this datagram looks like an osc message or bundle,
59
- if not the server won't even bother to call it and so no new
60
- threads/processes will be spawned.
61
- """
62
-
63
- def handle(self) -> None:
64
- LOG.debug("handle OSC 1.0 protocol")
65
- while True:
66
- lengthbuf = self.recvall(4)
67
- if lengthbuf == b"":
68
- break
69
- (length,) = struct.unpack("!I", lengthbuf)
70
- data = self.recvall(length)
71
- if data == b"":
72
- break
73
-
74
- resp = self.server.dispatcher.call_handlers_for_packet(
75
- data, self.client_address
76
- )
77
- # resp = _call_handlers_for_packet(data, self.server.dispatcher)
78
- for r in resp:
79
- if not isinstance(r, tuple):
80
- r = [r]
81
- msg = osc_message_builder.build_msg(r[0], r[1:])
82
- b = struct.pack("!I", len(msg.dgram))
83
- self.request.sendall(b + msg.dgram)
84
-
85
- def recvall(self, count: int) -> bytes:
86
- buf = b""
87
- while count > 0:
88
- newbuf = self.request.recv(count)
89
- if not newbuf:
90
- return b""
91
- buf += newbuf
92
- count -= len(newbuf)
93
- return buf
94
-
95
-
96
- class _TCPHandler1_1(socketserver.BaseRequestHandler):
97
- """Handles correct OSC1.1 messages.
65
+ class TCPHandler(socketserver.BaseRequestHandler):
66
+ """Handles correct OSC messages.
98
67
 
99
68
  Whether this will be run on its own thread, the server's or a whole new
100
69
  process depends on the server you instantiated, look at their documentation.
@@ -106,14 +75,12 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
106
75
  """
107
76
 
108
77
  def handle(self) -> None:
109
- LOG.debug("handle OSC 1.1 protocol")
110
- while True:
111
- packets = self.recvall()
112
- if not packets:
113
- break
114
-
115
- for p in packets:
116
- # resp = _call_handlers_for_packet(p, self.server.dispatcher)
78
+ assert isinstance(self.server, OSCTCPServer)
79
+
80
+ parser = FramingParser(self.server.framing)
81
+
82
+ while chunk := self.request.recv(16384):
83
+ for p in parser.feed(chunk):
117
84
  resp = self.server.dispatcher.call_handlers_for_packet(
118
85
  p, self.client_address
119
86
  )
@@ -121,27 +88,15 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
121
88
  if not isinstance(r, tuple):
122
89
  r = [r]
123
90
  msg = osc_message_builder.build_msg(r[0], r[1:])
124
- self.request.sendall(slip.encode(msg.dgram))
125
-
126
- def recvall(self) -> List[bytes]:
127
- buf = self.request.recv(4096)
128
- if not buf:
129
- return []
130
- # If the last byte is not an END marker there could be more data coming
131
- while buf[-1] != 192:
132
- newbuf = self.request.recv(4096)
133
- if not newbuf:
134
- # Maybe should raise an exception here?
135
- break
136
- buf += newbuf
137
-
138
- packets = [slip.decode(p) for p in buf.split(slip.END_END)]
139
- return packets
91
+ self.request.sendall(self.server.framing.encode(msg.dgram))
140
92
 
141
93
 
142
94
  class OSCTCPServer(socketserver.TCPServer):
143
95
  """Superclass for different flavors of OSCTCPServer"""
144
96
 
97
+ dispatcher: Dispatcher
98
+ framing: Framing
99
+
145
100
  def __init__(
146
101
  self,
147
102
  server_address: Tuple[str | bytes | bytearray, int],
@@ -150,9 +105,8 @@ class OSCTCPServer(socketserver.TCPServer):
150
105
  family: socket.AddressFamily | None = None,
151
106
  ):
152
107
  self.request_queue_size = 300
153
- self.mode = mode
154
- if mode not in [MODE_1_0, MODE_1_1]:
155
- raise ValueError("OSC Mode must be '1.0' or '1.1'")
108
+ self.dispatcher = dispatcher
109
+ self.framing = get_framing(mode)
156
110
 
157
111
  if family is not None:
158
112
  self.address_family = family
@@ -171,16 +125,7 @@ class OSCTCPServer(socketserver.TCPServer):
171
125
  # Fallback to default if resolution fails
172
126
  pass
173
127
 
174
- if self.mode == MODE_1_0:
175
- super().__init__(server_address, _TCPHandler1_0)
176
- else:
177
- super().__init__(server_address, _TCPHandler1_1)
178
- self._dispatcher = dispatcher
179
-
180
- @property
181
- def dispatcher(self):
182
- """Dispatcher accessor for handlers to dispatch osc messages."""
183
- return self._dispatcher
128
+ super().__init__(server_address, TCPHandler)
184
129
 
185
130
 
186
131
  class BlockingOSCTCPServer(OSCTCPServer):
@@ -230,8 +175,8 @@ class AsyncOSCTCPServer:
230
175
  """
231
176
  self._port = port
232
177
  self._server_address = server_address
233
- self._dispatcher = dispatcher
234
- self._mode = mode
178
+ self.dispatcher = dispatcher
179
+ self.framing = get_framing(mode)
235
180
 
236
181
  async def __aenter__(self):
237
182
  return self
@@ -245,9 +190,6 @@ class AsyncOSCTCPServer:
245
190
  self.handle, self._server_address, self._port
246
191
  )
247
192
 
248
- addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
249
- LOG.debug(f"Serving on {addrs}")
250
-
251
193
  async with self._server:
252
194
  await self._server.serve_forever()
253
195
 
@@ -255,86 +197,28 @@ class AsyncOSCTCPServer:
255
197
  self._server.close()
256
198
  await self._server.wait_closed()
257
199
 
258
- @property
259
- def dispatcher(self):
260
- return self._dispatcher
261
-
262
200
  async def handle(
263
201
  self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
264
202
  ) -> None:
203
+ try:
204
+ await self._handle(reader, writer)
205
+ finally:
206
+ if writer.can_write_eof():
207
+ writer.write_eof()
208
+ writer.close()
209
+ await writer.wait_closed()
210
+
211
+ async def _handle(
212
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
213
+ ) -> None:
214
+ parser = FramingParser(self.framing)
265
215
  client_address = ("", 0)
266
216
  sock = writer.transport.get_extra_info("socket")
267
217
  if sock is not None:
268
218
  client_address = sock.getpeername()
269
219
 
270
- if self._mode == MODE_1_1:
271
- await self.handle_1_1(reader, writer, client_address)
272
- else:
273
- await self.handle1_0(reader, writer, client_address)
274
- writer.write_eof()
275
- LOG.debug("Close the connection")
276
- writer.close()
277
- await writer.wait_closed()
278
-
279
- async def handle1_0(
280
- self,
281
- reader: asyncio.StreamReader,
282
- writer: asyncio.StreamWriter,
283
- client_address: Tuple[str, int],
284
- ) -> None:
285
- LOG.debug("Incoming socket open 1.0")
286
- while True:
287
- try:
288
- buf = await reader.read(4)
289
- except Exception as e:
290
- LOG.exception("Read error", e)
291
- return
292
- if buf == b"":
293
- break
294
- (length,) = struct.unpack("!I", buf)
295
- buf = b""
296
- while length > 0:
297
- newbuf = await reader.read(length)
298
- if not newbuf:
299
- break
300
- buf += newbuf
301
- length -= len(newbuf)
302
-
303
- result = await self.dispatcher.async_call_handlers_for_packet(
304
- buf, client_address
305
- )
306
- for r in result:
307
- if not isinstance(r, tuple):
308
- r = [r]
309
- msg = osc_message_builder.build_msg(r[0], r[1:])
310
- b = struct.pack("!I", len(msg.dgram))
311
- writer.write(b + msg.dgram)
312
- await writer.drain()
313
-
314
- async def handle_1_1(
315
- self,
316
- reader: asyncio.StreamReader,
317
- writer: asyncio.StreamWriter,
318
- client_address: Tuple[str, int],
319
- ) -> None:
320
- LOG.debug("Incoming socket open 1.1")
321
- while True:
322
- try:
323
- buf = await reader.read(4096)
324
- except Exception as e:
325
- LOG.exception("Read error", e)
326
- return
327
- if buf == b"":
328
- break
329
- while len(buf) > 0 and buf[-1] != 192:
330
- newbuf = await reader.read(4096)
331
- if not newbuf:
332
- # Maybe should raise an exception here?
333
- break
334
- buf += newbuf
335
-
336
- packets = [slip.decode(p) for p in buf.split(slip.END_END)]
337
- for p in packets:
220
+ while chunk := await reader.read(16384):
221
+ for p in parser.feed(chunk):
338
222
  result = await self.dispatcher.async_call_handlers_for_packet(
339
223
  p, client_address
340
224
  )
@@ -342,5 +226,5 @@ class AsyncOSCTCPServer:
342
226
  if not isinstance(r, tuple):
343
227
  r = [r]
344
228
  msg = osc_message_builder.build_msg(r[0], r[1:])
345
- writer.write(slip.encode(msg.dgram))
229
+ writer.write(self.framing.encode(msg.dgram))
346
230
  await writer.drain()
@@ -0,0 +1,166 @@
1
+ """OSC message codecs for different framing strategies."""
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from struct import pack, unpack_from
5
+
6
+ from pythonosc.parsing.slip import (
7
+ END,
8
+ decode as slip_decode,
9
+ encode as slip_encode,
10
+ )
11
+
12
+
13
+ class Framing(metaclass=ABCMeta):
14
+ """Base class for OSC stream framing strategies."""
15
+
16
+ @abstractmethod
17
+ def encode(self, dgram: bytes) -> bytes:
18
+ """Encode a single OSC datagram for transmission on a byte stream.
19
+
20
+ Args:
21
+ dgram: Raw OSC datagram bytes.
22
+
23
+ Returns:
24
+ The framed byte sequence to write to the stream.
25
+ """
26
+
27
+ @abstractmethod
28
+ def decode_stream(
29
+ self, buf: bytes | bytearray, start: int = 0
30
+ ) -> tuple[list[bytes], int]:
31
+ """Decode complete OSC datagrams from a byte buffer.
32
+
33
+ Args:
34
+ buf: Bytes received from a stream.
35
+ start: The index in ``buf`` to start decoding from.
36
+
37
+ Returns:
38
+ A tuple ``(messages, bytes consumed)``.
39
+ """
40
+
41
+
42
+ class SLIPFraming(Framing):
43
+ """SLIP (RFC 1055) framing for OSC 1.1.
44
+
45
+ See the "OSC Delivery Specification 1.1" section from the 2009 NIME paper for details.
46
+ """
47
+
48
+ def encode(self, dgram: bytes) -> bytes:
49
+ return slip_encode(dgram)
50
+
51
+ def decode_stream(
52
+ self, buf: bytes | bytearray, start: int = 0
53
+ ) -> tuple[list[bytes], int]:
54
+ pos = start
55
+ messages = []
56
+
57
+ scan = start
58
+ while (end := buf.find(END, scan)) >= 0:
59
+ if end > scan:
60
+ messages.append(slip_decode(bytes(buf[scan:end])))
61
+ pos = end + len(END)
62
+ scan = end + len(END)
63
+
64
+ # Consume trailing delimiters, but retain an incomplete packet.
65
+ if scan == len(buf):
66
+ pos = scan
67
+
68
+ return messages, pos - start
69
+
70
+
71
+ class BinaryLengthFraming(Framing):
72
+ """Length-prefixed framing for OSC 1.0.
73
+
74
+ See the "OSC Packets" section from OSC 1.0 Specification for details.
75
+ """
76
+
77
+ def encode(self, dgram: bytes) -> bytes:
78
+ if dgram:
79
+ return pack("!I", len(dgram)) + dgram
80
+ return b""
81
+
82
+ def decode_stream(
83
+ self, buf: bytes | bytearray, start: int = 0
84
+ ) -> tuple[list[bytes], int]:
85
+ messages = []
86
+ pos = start
87
+
88
+ while (data_pos := pos + 4) < len(buf):
89
+ (length,) = unpack_from("!I", buf, pos)
90
+ end = data_pos + length
91
+
92
+ if end > len(buf):
93
+ break
94
+
95
+ messages.append(bytes(buf[data_pos:end]))
96
+
97
+ pos = end
98
+
99
+ return messages, pos - start
100
+
101
+
102
+ class FramingParser:
103
+ """Stateful stream decoder for (framed) OSC byte stream.
104
+
105
+ Not safe for concurrent use.
106
+
107
+ Args:
108
+ framing: Strategy used to decode buffered stream data.
109
+ max_buffer_size: Maximum size of the internal buffer.
110
+ """
111
+
112
+ __slots__ = ("buf", "start", "framing", "max_buffer_size")
113
+
114
+ def __init__(self, framing: Framing, max_buffer_size: int = 1048576) -> None:
115
+ self.buf = bytearray()
116
+ self.start = 0
117
+ self.framing = framing
118
+ self.max_buffer_size = max_buffer_size
119
+
120
+ def feed(self, chunk: bytes | bytearray | memoryview) -> list[bytes]:
121
+ """Append stream data to the internal buffer and return any complete OSC datagrams.
122
+
123
+ Args:
124
+ chunk: Newly received bytes from the stream.
125
+
126
+ Returns:
127
+ A list of complete, decoded OSC datagrams.
128
+ """
129
+
130
+ new_buf_len = len(self.buf) + len(chunk)
131
+
132
+ if (pending := new_buf_len - self.start) > self.max_buffer_size:
133
+ raise ValueError(f"Buffer is too large: {pending} > {self.max_buffer_size}")
134
+
135
+ if new_buf_len > self.max_buffer_size:
136
+ del self.buf[: self.start]
137
+ self.start = 0
138
+
139
+ self.buf.extend(chunk)
140
+
141
+ messages, consumed = self.framing.decode_stream(self.buf, self.start)
142
+ available = len(self.buf) - self.start
143
+
144
+ assert 0 <= consumed <= available, (
145
+ f"misbehaving framing decoder: {consumed=} {available=}"
146
+ )
147
+
148
+ self.start += consumed
149
+
150
+ return messages
151
+
152
+ @property
153
+ def remaining(self) -> bytes:
154
+ """Return any unparsed bytes."""
155
+
156
+ return bytes(self.buf[self.start :])
157
+
158
+ def reset(self) -> None:
159
+ """Clear the internal buffer (discard unparsed data)."""
160
+
161
+ del self.buf[:]
162
+ self.start = 0
163
+
164
+
165
+ SLIP_FRAMING: Framing = SLIPFraming()
166
+ BINARY_LENGTH_FRAMING: Framing = BinaryLengthFraming()
@@ -86,6 +86,8 @@ def get_string(dgram: bytes, start_index: int) -> Tuple[str, int]:
86
86
 
87
87
  data_str = dgram[start_index : start_index + offset]
88
88
  return data_str.decode("utf-8"), start_index + total_len
89
+ except UnicodeDecodeError as error:
90
+ raise ParseError(f"Could not decode OSC string: {error}") from error
89
91
  except IndexError as ie:
90
92
  raise ParseError(f"Could not parse datagram {ie}")
91
93
  except TypeError as te:
@@ -307,7 +309,7 @@ def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
307
309
 
308
310
  Args:
309
311
  dgram: A datagram packet.
310
- start_index: An index where the float starts in the datagram.
312
+ start_index: An index where the blob starts in the datagram.
311
313
 
312
314
  Returns:
313
315
  A tuple containing the blob and the new end index.
@@ -315,13 +317,17 @@ def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
315
317
  Raises:
316
318
  ParseError if the datagram could not be parsed.
317
319
  """
318
- size, int_offset = get_int(dgram, start_index)
319
- # Make the size a multiple of 32 bits.
320
- total_size = size + (-size % _BLOB_DGRAM_PAD)
321
- end_index = int_offset + size
322
- if end_index - start_index > len(dgram[start_index:]):
320
+ if start_index < 0:
321
+ raise ParseError("start_index < 0")
322
+ size, payload_start = get_int(dgram, start_index)
323
+ if size < 0:
324
+ raise ParseError("Blob length cannot be negative")
325
+
326
+ payload_end = payload_start + size
327
+ next_index = payload_end + (-size % _BLOB_DGRAM_PAD)
328
+ if next_index > len(dgram):
323
329
  raise ParseError("Datagram is too short.")
324
- return dgram[int_offset : int_offset + size], int_offset + total_size
330
+ return dgram[payload_start:payload_end], next_index
325
331
 
326
332
 
327
333
  def write_blob(val: bytes) -> bytes: