diff --git a/README.rst b/README.md similarity index 62% rename from README.rst rename to README.md index 5b5d653..933c7ef 100644 --- a/README.rst +++ b/README.md @@ -1,106 +1,84 @@ - -.. image:: https://readthedocs.org/projects/sliplib/badge/?version=latest - :target: http://sliplib.readthedocs.org/en/master/?badge=master - :alt: ReadTheDocs Documentation Status - -.. image:: https://travis-ci.org/rhjdjong/SlipLib.svg - :target: https://travis-ci.org/rhjdjong/SlipLib - :alt: Travis Test Status - -.. image:: https://ci.appveyor.com/api/projects/status/d1nwwn34xoaxh3tt/branch/master?svg=true - :target: https://ci.appveyor.com/project/RuuddeJong/sliplib/branch/master - :alt: AppVeyor Test Status - - -============================================== -``sliplib`` --- A module for the SLIP protocol -============================================== - - -The `sliplib` module implements the encoding and decoding -functionality for SLIP packets, as described in :rfc:`1055`. -It defines encoding, decoding, and validation functions, -as well as a driver class that can be used to implement -a SLIP protocol stack, and higher-level classes that -apply the SLIP protocol to TCP connections or IO streams. -Read the `documentation `_ -for detailed information. - -Background -========== - -The SLIP protocol is described in :rfc:`1055` (:title:`A Nonstandard for -Transmission of IP Datagrams over Serial Lines: SLIP`, J. Romkey, -June 1988). The original purpose of the protocol is -to provide a mechanism to indicate the boundaries of IP packets, -in particular when the IP packets are sent over a connection that -does not provide a framing mechanism, such as serial lines or -dial-up connections. - -There is, however, nothing specific to IP in the SLIP protocol. -SLIP offers a generic framing method that can be used for any -type of data that must be transmitted over a (continuous) byte stream. -In fact, the main reason for creating this module -was the need to communicate with a third-party application that -used SLIP over TCP (which is a continuous byte stream) -to frame variable length data structures. - - -Usage -===== - -Installation ------------- - -To install the `sliplib` module, use - -.. code:: - - pip install sliplib - -Low-level usage ---------------- - -The recommended basic usage is to run all encoding and decoding operations -through an instantiation `driver` of the `Driver` class, in combination -with the appropriate I/O code. -The `Driver` class itself works without any I/O, and can therefore be used with -any networking code, or any bytestream like pipes, serial I/O, etc. -It can work in synchronous as well as in asynchronous environments. - -The `Driver` class offers the methods -`send` and `receive` to handle -the conversion between messages and SLIP-encoded packets. - -High-level usage ----------------- - -The module also provides a `SlipWrapper` abstract baseclass -that provides the methods `send_msg` and `recv_msg` to send -and receive single SLIP-encoded messages. This base class -wraps an instance of the `Driver` class with a user-provided stream. - -Two concrete subclasses of `SlipWrapper` are provided: - -* `SlipStream` allows the wrapping of a byte IO stream. -* `SlipSocket` allows the wrapping of a TCP socket. - -In addition, the module also provides a `SlipRequestHandler` -to facilitate the creation of TCP servers that can handle -SLIP-encoded messages. - - -Error Handling -============== - -Contrary to the reference implementation described in :rfc:`1055`, -which chooses to essentially ignore protocol errors, -the functions and classes in the `sliplib` module -use a `ProtocolError` exception -to indicate protocol errors, i.e. SLIP packets with invalid byte sequences. -The `Driver` class raises the `ProtocolError` exception -as soon as a complete SLIP packet with an invalid byte sequence is received. -The `SlipWrapper` class and its subclasses catch the `ProtocolError`\s -raised by the `Driver` class, and re-raise them when -an attempt is made to read the contents of a SLIP packet that contained -invalid data. +[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) +[![ReadTheDocs Documentation Status](https://readthedocs.org/projects/sliplib/badge/?version=latest)](http://sliplib.readthedocs.org/en/master/?badge=master) + + +# ``sliplib`` --- A module for the SLIP protocol + +The `sliplib` module implements the encoding and decoding +functionality for SLIP packets, as described in :rfc:`1055`. +It defines encoding, decoding, and validation functions, +as well as a driver class that can be used to implement +a SLIP protocol stack, and higher-level classes that +apply the SLIP protocol to TCP connections or IO streams. +Read the [documentation](http://sliplib.readthedocs.org/en/master/) +for detailed information. + +## Background + +The SLIP protocol is described in +[RFC 1055](https://www.rfc-editor.org/rfc/rfc1055.html) +(`A Nonstandard for Transmission of IP Datagrams over Serial Lines: SLIP`, +J. Romkey, June 1988). +The original purpose of the protocol is +to provide a mechanism to indicate the boundaries of IP packets, +in particular when the IP packets are sent over a connection that +does not provide a framing mechanism, such as serial lines or +dial-up connections. + +There is, however, nothing specific to IP in the SLIP protocol. +SLIP offers a generic framing method that can be used for any +type of data that must be transmitted over a (continuous) byte stream. +In fact, the main reason for creating this module +was the need to communicate with a third-party application that +used SLIP over TCP (which is a continuous byte stream) +to frame variable length data structures. + + +## Usage + +### Installation + +To install the `sliplib` module, use + + pip install sliplib + +### Low-level usage + +The recommended basic usage is to run all encoding and decoding operations +through an instantiation `driver` of the `Driver` class, in combination +with the appropriate I/O code. +The `Driver` class itself works without any I/O, and can therefore be used with +any networking code, or any bytestream like pipes, serial I/O, etc. +It can work in synchronous as well as in asynchronous environments. + +The `Driver` class offers the methods +`send` and `receive` to handle +the conversion between messages and SLIP-encoded packets. + +## High-level usage + +The module also provides a `SlipWrapper` abstract baseclass +that provides the methods `send_msg` and `recv_msg` to send +and receive single SLIP-encoded messages. This base class +wraps an instance of the `Driver` class with a user-provided stream. + +Two concrete subclasses of `SlipWrapper` are provided: + +* `SlipStream` allows the wrapping of a byte IO stream. +* `SlipSocket` allows the wrapping of a TCP socket. + +In addition, the module also provides a `SlipRequestHandler` +to facilitate the creation of TCP servers that can handle +SLIP-encoded messages. + + +## Error Handling + +Contrary to the reference implementation described in :rfc:`1055`, +which chooses to essentially ignore protocol errors, +the functions and classes in the `sliplib` module +use a `ProtocolError` exception +to indicate protocol errors, i.e. SLIP packets with invalid byte sequences. +The `Driver` class raises the `ProtocolError` exception +as when the application it tries to decode a SLIP packet with an invalid byte sequence. +Subsequent messages can be read after the exception has been handled by the application. diff --git a/pyproject.toml b/pyproject.toml index df9f987..db1d7d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "sliplib" dynamic = ["version"] description = "Slip package" -readme = "README.rst" +readme = "README.md" license = "MIT" authors = [ { name = "Ruud de Jong", email = "ruud.de.jong@xs4all.nl" }, diff --git a/src/sliplib/slip.py b/src/sliplib/slip.py index 6aaeedd..10f2e19 100644 --- a/src/sliplib/slip.py +++ b/src/sliplib/slip.py @@ -37,12 +37,7 @@ .. automethod:: send .. automethod:: receive - - To enable recovery from a :exc:`ProtocolError`, the - :class:`Driver` class offers the following attribute and method: - - .. autoattribute:: messages - .. automethod:: flush + .. automethod:: get """ from __future__ import annotations @@ -147,10 +142,10 @@ def send(self, message: bytes) -> bytes: return encode(message) def receive(self, data: bytes | int) -> None: - """Decodes data to extract the SLIP-encoded messages. + """Receive data that contains SLIP-encoded messages. Processes :obj:`data`, which must be a bytes-like object, - and extracts and buffers the SLIP messages contained therein. + and extracts and buffers the encoded SLIP messages contained therein. A non-terminated SLIP packet in :obj:`data` is also buffered, and processed with the next call to :meth:`receive`. @@ -167,7 +162,7 @@ def receive(self, data: bytes | int) -> None: None. .. versionchanged:: 0.7 - `receive()` no longer returns a list of decoded messages. + :meth:`receive()` no longer returns a list of decoded messages. Use :meth:`get()` to retrieve messages. """ # When a single byte is fed into this function @@ -205,17 +200,14 @@ def receive(self, data: bytes | int) -> None: for packet in new_packets: self._packets.put(packet) - def get(self, *, block: bool = True, timeout: float | None = None) -> bytes | None: + def get(self) -> bytes | None: """Get the next decoded message. Remove and decode a SLIP packet from the internal buffer, and return the resulting message. - If `block` is `True` and `timeout` is `None`(the default), then this method blocks until a message is available. - If `timeout` is a positive number, the blocking will last for at most `timeout` seconds, - and the method will return `None` if no message became available in that time. - If `block` is `False` the method returns immediately with either a message or `None`. Returns: - A decoded SLIP message, or an empty bytestring `b""` if no further message will come available. + A decoded SLIP message, `None` if no messages are available, + or an empty bytestring `b""` if no further messages will come available. Raises: ProtocolError: When the packet that contained the message had an invalid byte sequence. @@ -223,7 +215,7 @@ def get(self, *, block: bool = True, timeout: float | None = None) -> bytes | No .. versionadded:: 0.7 """ try: - packet = self._packets.get(block, timeout) + packet = self._packets.get(block=False) except Empty: return b"" if self._finished else None diff --git a/src/sliplib/slipserver.py b/src/sliplib/slipserver.py index 1b44a42..a5cf579 100644 --- a/src/sliplib/slipserver.py +++ b/src/sliplib/slipserver.py @@ -40,13 +40,13 @@ class SlipRequestHandler(BaseRequestHandler): """ def __init__(self, request: socket.socket | SlipSocket, client_address: TCPAddress, server: TCPServer): - """Initializes the request handler. + """:class:`SlipRequestHandler` must be initialized with a request (socket) and a client TCP address. The type of the :attr:`request` parameter depends on the type of server that instantiates the request handler. - If the server is a SlipServer, then :attr:`request` is a SlipSocket. - Otherwise, it is a regular socket, and must be wrapped in a SlipSocket - before it can be used. + If the server is a :class:`SlipServer`, then :attr:`request` is a SlipSocket. + Otherwise, it is a regular socket, and will be wrapped in a SlipSocket + before it is used. Args: request: diff --git a/src/sliplib/slipwrapper.py b/src/sliplib/slipwrapper.py index b5c2d4c..af3cf07 100644 --- a/src/sliplib/slipwrapper.py +++ b/src/sliplib/slipwrapper.py @@ -90,8 +90,8 @@ def recv_bytes(self) -> bytes: The convention used within the :class:`SlipWrapper` class is that :meth:`recv_bytes` returns an empty bytes object to indicate that the end of - the byte stream has been reached and no further data will - be received. Derived implementations must ensure that + the byte stream has been reached and no further data will follow. + Derived implementations must ensure that this convention is followed. Returns: @@ -113,13 +113,8 @@ def recv_msg(self) -> bytes: Returns: bytes: A SLIP-decoded message - - Raises: - ProtocolError: when a SLIP protocol error has been encountered. - A subsequent call to :meth:`recv_msg` (after handling the exception) - will return the message from the next packet. """ - while (message := self.driver.get(block=False)) is None: + while (message := self.driver.get()) is None: data = self.recv_bytes() self.driver.receive(data) return message diff --git a/tests/unit/test_slip.py b/tests/unit/test_slip.py index ebd7843..b538f20 100644 --- a/tests/unit/test_slip.py +++ b/tests/unit/test_slip.py @@ -135,23 +135,23 @@ def test_single_message_decoding(self) -> None: msg = b"hallo" packet = encode(msg) self.driver.receive(packet) - assert self.driver.get(timeout=0.5) == msg + assert self.driver.get() == msg def test_multi_message_decoding(self) -> None: """Test decoding of a byte string with multiple packets.""" msgs = [b"hi", b"there"] packet = END + msgs[0] + END + msgs[1] + END self.driver.receive(packet) - assert self.driver.get(timeout=0.5) == msgs[0] - assert self.driver.get(timeout=0.5) == msgs[1] + assert self.driver.get() == msgs[0] + assert self.driver.get() == msgs[1] def test_multiple_end_bytes_are_ignored_during_decoding(self) -> None: """Test decoding of a byte string with multiple packets.""" msgs = [b"hi", b"there"] packet = END + END + msgs[0] + END + END + END + END + msgs[1] + END + END + END self.driver.receive(packet) - assert self.driver.get(timeout=0.5) == msgs[0] - assert self.driver.get(timeout=0.5) == msgs[1] + assert self.driver.get() == msgs[0] + assert self.driver.get() == msgs[1] def test_split_message_decoding(self) -> None: """Test that receives only returns the message after the complete packet has been received. @@ -162,19 +162,19 @@ def test_split_message_decoding(self) -> None: packet = END + msg for byte_ in packet: self.driver.receive(byte_) - assert self.driver.get(block=False) is None + assert self.driver.get() is None self.driver.receive(END) - assert self.driver.get(timeout=0.5) == msg + assert self.driver.get() == msg def test_flush_buffers_with_empty_packet(self) -> None: """Test that receiving an empty byte string results in completion of the pending packet.""" expected_msg_list = [b"hi", b"there"] packet = END + expected_msg_list[0] + END + expected_msg_list[1] self.driver.receive(packet) - assert self.driver.get(timeout=0.5) == expected_msg_list[0] - assert self.driver.get(block=False) is None + assert self.driver.get() == expected_msg_list[0] + assert self.driver.get() is None self.driver.receive(b"") - assert self.driver.get(timeout=0.5) == expected_msg_list[1] + assert self.driver.get() == expected_msg_list[1] @pytest.mark.parametrize("message", [b"with" + ESC + b" error", b"with trailing" + ESC]) def test_packet_with_protocol_error(self, message: bytes) -> None: @@ -182,7 +182,7 @@ def test_packet_with_protocol_error(self, message: bytes) -> None: packet = END + message + END self.driver.receive(packet) with pytest.raises(ProtocolError) as exc_info: - self.driver.get(timeout=0.5) + self.driver.get() assert exc_info.value.args == (message,) def test_messages_before_invalid_packets(self) -> None: @@ -190,9 +190,9 @@ def test_messages_before_invalid_packets(self) -> None: msgs = [b"hallo", b"with" + ESC + b" error"] packet = END + END.join(msgs) + END self.driver.receive(packet) - assert self.driver.get(timeout=0.5) == msgs[0] + assert self.driver.get() == msgs[0] with pytest.raises(ProtocolError) as exc_info: - self.driver.get(timeout=0.5) + self.driver.get() assert exc_info.value.args == (msgs[1],) def test_messages_after_invalid_packets(self) -> None: @@ -201,9 +201,9 @@ def test_messages_after_invalid_packets(self) -> None: packet = END + END.join(msgs) + END self.driver.receive(packet) with pytest.raises(ProtocolError) as exc_info: - self.driver.get(timeout=0.5) + self.driver.get() assert exc_info.value.args == (msgs[0],) - assert self.driver.get(timeout=0.5) == msgs[1] + assert self.driver.get() == msgs[1] def test_subsequent_packets_with_wrong_escape_sequence(self) -> None: """Test that each invalid packet results in a protocol error.""" @@ -216,12 +216,12 @@ def test_subsequent_packets_with_wrong_escape_sequence(self) -> None: ] packet = END + END.join(msgs) + END self.driver.receive(packet) - assert self.driver.get(timeout=0.5) == msgs[0] + assert self.driver.get() == msgs[0] with pytest.raises(ProtocolError) as exc_info: - self.driver.get(timeout=0.5) + self.driver.get() assert exc_info.value.args == (msgs[1],) - assert self.driver.get(timeout=0.5) == msgs[2] + assert self.driver.get() == msgs[2] with pytest.raises(ProtocolError) as exc_info: - self.driver.get(timeout=0.5) + self.driver.get() assert exc_info.value.args == (msgs[3],) - assert self.driver.get(timeout=0.5) == msgs[4] + assert self.driver.get() == msgs[4]