From 8527705a13b12da48bfdfb43413f6c2564a622e6 Mon Sep 17 00:00:00 2001 From: leo <80651389+civilwarinfrance@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:15:39 -0800 Subject: [PATCH] Decode SLIP packets independently of stream chunk boundaries --- pythonosc/parsing/framing.py | 18 ++++++------- pythonosc/test/parsing/test_framing.py | 36 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/pythonosc/parsing/framing.py b/pythonosc/parsing/framing.py index 6e19dee..4728242 100644 --- a/pythonosc/parsing/framing.py +++ b/pythonosc/parsing/framing.py @@ -5,7 +5,6 @@ from pythonosc.parsing.slip import ( END, - END_END, decode as slip_decode, encode as slip_encode, ) @@ -55,15 +54,16 @@ def decode_stream( pos = start messages = [] - while (end := buf.find(END_END, pos)) >= 0: - if end > pos: - messages.append(slip_decode(bytes(buf[pos:end]))) + scan = start + while (end := buf.find(END, scan)) >= 0: + if end > scan: + messages.append(slip_decode(bytes(buf[scan:end]))) + pos = end + len(END) + scan = end + len(END) - pos = end + len(END) - - if buf.endswith(END): - messages.append(slip_decode(bytes(buf[pos:end]))) - pos = len(buf) + # Consume trailing delimiters, but retain an incomplete packet. + if scan == len(buf): + pos = scan return messages, pos - start diff --git a/pythonosc/test/parsing/test_framing.py b/pythonosc/test/parsing/test_framing.py index 72361de..4915479 100644 --- a/pythonosc/test/parsing/test_framing.py +++ b/pythonosc/test/parsing/test_framing.py @@ -70,6 +70,42 @@ def framing(self) -> Framing: def test_encode(self, framing: Framing) -> None: assert framing.encode(b"te\xc0st") == b"\xc0te\xdb\xdcst\xc0" + @mark.parametrize("prefix", [b"", b"\xc0"]) + def test_single_end_separates_packets( + self, framing: Framing, prefix: bytes + ) -> None: + data = prefix + b"one\xc0two\xc0" + assert framing.decode_stream(data) == ([b"one", b"two"], len(data)) + + @mark.parametrize("count", [1, 2, 3, 5]) + def test_end_markers_do_not_emit_empty_packets( + self, framing: Framing, count: int + ) -> None: + data = b"\xc0" * count + assert framing.decode_stream(data) == ([], len(data)) + + @mark.parametrize("leading_end", [False, True]) + def test_every_stream_split_preserves_packets( + self, framing: Framing, leading_end: bool + ) -> None: + expected = [b"one", b"two\xc0\xdb", b"three"] + data = b"".join(framing.encode(value)[not leading_end :] for value in expected) + for split in range(len(data) + 1): + parser = FramingParser(framing) + actual = parser.feed(data[:split]) + parser.feed(data[split:]) + assert actual == expected, split + assert parser.remaining == b"" + + def test_bytewise_stream_preserves_packets(self, framing: Framing) -> None: + parser = FramingParser(framing) + expected = [b"one", b"two\xc0\xdb"] + data = b"".join(framing.encode(value) for value in expected) + actual = [] + for byte in data: + actual.extend(parser.feed(bytes([byte]))) + assert actual == expected + assert parser.remaining == b"" + class TestBinaryLengthFraming(CommonFramingTest): @fixture