Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/pygwire/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
FrontendConnection,
)
from pygwire.constants import ConnectionPhase, ProtocolVersion, TransactionStatus
from pygwire.exceptions import ProtocolError, PygwireError
from pygwire.exceptions import (
DecodingError,
FramingError,
ProtocolError,
PygwireError,
StateMachineError,
)
from pygwire.state_machine import (
BackendStateMachine,
FrontendStateMachine,
StateMachineError,
)

__version__ = version("pygwire")
Expand All @@ -24,6 +29,8 @@
"BackendStateMachine",
"Connection",
"ConnectionPhase",
"DecodingError",
"FramingError",
"FrontendConnection",
"FrontendMessageDecoder",
"FrontendStateMachine",
Expand Down
2 changes: 1 addition & 1 deletion src/pygwire/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ def on_send(self, data: bytes) -> None:

from pygwire.codec import BackendMessageDecoder, FrontendMessageDecoder
from pygwire.constants import ConnectionPhase
from pygwire.exceptions import StateMachineError
from pygwire.messages import PGMessage
from pygwire.state_machine import (
BackendStateMachine,
FrontendStateMachine,
StateMachineError,
)

logger = logging.getLogger(__name__)
Expand Down
15 changes: 15 additions & 0 deletions src/pygwire/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""Pygwire exception hierarchy."""

__all__ = [
"DecodingError",
"FramingError",
"ProtocolError",
"PygwireError",
"StateMachineError",
]


Expand All @@ -12,3 +15,15 @@ class PygwireError(Exception):

class ProtocolError(PygwireError):
"""Raised when protocol framing or content is invalid."""


class FramingError(ProtocolError):
"""Raised when message framing is invalid (size, identifier, truncation)."""


class DecodingError(ProtocolError):
"""Raised when a message payload cannot be decoded."""


class StateMachineError(ProtocolError):
"""Raised when an invalid message is sent/received for the current state."""
22 changes: 11 additions & 11 deletions src/pygwire/framing.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from typing import TYPE_CHECKING

from pygwire.constants import ConnectionPhase, MessageDirection
from pygwire.exceptions import ProtocolError
from pygwire.exceptions import FramingError
from pygwire.messages import (
NEGOTIATION_REGISTRY,
STANDARD_REGISTRY,
Expand Down Expand Up @@ -82,7 +82,7 @@ def try_parse(
(message, bytes_consumed) if successful, None if insufficient data

Raises:
ProtocolError: If message is malformed or unknown
FramingError: If message is malformed or unknown
"""
...

Expand Down Expand Up @@ -117,7 +117,7 @@ def try_parse(

(length,) = _LENGTH_STRUCT.unpack_from(buf, pos)
if length > self._max_message_size:
raise ProtocolError(
raise FramingError(
f"Startup message length {length} exceeds maximum allowed size "
f"({self._max_message_size})"
)
Expand All @@ -129,17 +129,17 @@ def try_parse(
payload_end = pos + length
payload = buf[payload_start:payload_end]
if len(payload) < 4:
raise ProtocolError("Startup message payload too short for version code")
raise FramingError("Startup message payload too short for version code")

(version_code,) = _LENGTH_STRUCT.unpack_from(payload)

msg_cls = STARTUP_REGISTRY.lookup(version_code)
if msg_cls is None:
raise ProtocolError(f"Unknown startup message version code: {version_code:#010x}")
raise FramingError(f"Unknown startup message version code: {version_code:#010x}")
try:
msg = msg_cls.decode(payload)
except struct.error as e:
raise ProtocolError(f"{msg_cls.__name__} message truncated or malformed: {e}") from e
raise FramingError(f"{msg_cls.__name__} message truncated or malformed: {e}") from e

return msg, length

Expand Down Expand Up @@ -170,13 +170,13 @@ def try_parse(

msg_cls = NEGOTIATION_REGISTRY.lookup(byte_value, phase)
if msg_cls is None:
raise ProtocolError(f"Unknown negotiation byte: {byte_value!r} in phase {phase.name}")
raise FramingError(f"Unknown negotiation byte: {byte_value!r} in phase {phase.name}")

payload = buf[pos : pos + 1]
try:
msg = msg_cls.decode(payload)
except struct.error as e:
raise ProtocolError(f"{msg_cls.__name__} message malformed: {e}") from e
raise FramingError(f"{msg_cls.__name__} message malformed: {e}") from e

return msg, 1

Expand Down Expand Up @@ -212,7 +212,7 @@ def try_parse(
identifier = bytes((buf[pos],))
(length,) = _LENGTH_STRUCT.unpack_from(buf, pos + 1)
if length > self._max_message_size:
raise ProtocolError(
raise FramingError(
f"Message length {length} exceeds maximum allowed size ({self._max_message_size})"
)

Expand All @@ -226,14 +226,14 @@ def try_parse(

msg_cls = STANDARD_REGISTRY.lookup(identifier, phase, direction)
if msg_cls is None:
raise ProtocolError(
raise FramingError(
f"Unknown message identifier: {identifier!r} in phase {phase.name} "
f"for direction {direction.value}"
)
try:
msg = msg_cls.decode(payload)
except struct.error as e:
raise ProtocolError(f"{msg_cls.__name__} message truncated or malformed: {e}") from e
raise FramingError(f"{msg_cls.__name__} message truncated or malformed: {e}") from e

return msg, total

Expand Down
14 changes: 7 additions & 7 deletions src/pygwire/messages/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import ClassVar, Self

from pygwire.constants import ConnectionPhase, MessageDirection
from pygwire.exceptions import ProtocolError
from pygwire.exceptions import DecodingError
from pygwire.messages._base import BackendMessage, FrontendMessage, _read_cstring

from ._registry import NEGOTIATION_REGISTRY, STANDARD_REGISTRY
Expand Down Expand Up @@ -41,14 +41,14 @@ def to_wire(self) -> bytes:
@classmethod
def decode(cls, payload: memoryview) -> Self:
if len(payload) < 1:
raise ProtocolError("SSLResponse payload is empty")
raise DecodingError("SSLResponse payload is empty")
byte = bytes(payload[0:1])
if byte == b"S":
return cls(accepted=True)
elif byte == b"N":
return cls(accepted=False)
else:
raise ProtocolError(f"Unexpected SSL response byte: {byte!r}")
raise DecodingError(f"Unexpected SSL response byte: {byte!r}")


@NEGOTIATION_REGISTRY.register(b"G", ConnectionPhase.GSS_NEGOTIATION)
Expand All @@ -75,14 +75,14 @@ def to_wire(self) -> bytes:
@classmethod
def decode(cls, payload: memoryview) -> Self:
if len(payload) < 1:
raise ProtocolError("GSSResponse payload is empty")
raise DecodingError("GSSResponse payload is empty")
byte = bytes(payload[0:1])
if byte == b"G":
return cls(accepted=True)
elif byte == b"N":
return cls(accepted=False)
else:
raise ProtocolError(f"Unexpected GSS response byte: {byte!r}")
raise DecodingError(f"Unexpected GSS response byte: {byte!r}")


# ═══════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -137,7 +137,7 @@ def decode(cls, payload: memoryview) -> Self:
(code,) = _INT32.unpack_from(payload)
sub_cls = _AUTH_SUBTYPE_REGISTRY.get(code)
if sub_cls is None:
raise ProtocolError(f"Unknown authentication code: {code}")
raise DecodingError(f"Unknown authentication code: {code}")
return sub_cls.decode(payload) # type: ignore[return-value]


Expand Down Expand Up @@ -323,7 +323,7 @@ def decode(cls, payload: memoryview) -> Self:
try:
pwd, _ = _read_cstring(payload, 0)
return cls(password=pwd)
except ProtocolError:
except DecodingError:
return cls(password=bytes(payload))


Expand Down
4 changes: 2 additions & 2 deletions src/pygwire/messages/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dataclasses import dataclass
from typing import ClassVar, Self

from pygwire.exceptions import ProtocolError
from pygwire.exceptions import DecodingError

# ---------------------------------------------------------------------------
# Base message classes
Expand Down Expand Up @@ -123,4 +123,4 @@ def _read_cstring(payload: memoryview, offset: int) -> tuple[str, int]:
value = bytes(payload[offset:end]).decode("utf-8")
return value, end + 1
except ValueError:
raise ProtocolError("Unterminated string in payload") from None
raise DecodingError("Unterminated string in payload") from None
6 changes: 1 addition & 5 deletions src/pygwire/state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

from pygwire import messages
from pygwire.constants import ConnectionPhase
from pygwire.exceptions import ProtocolError
from pygwire.exceptions import StateMachineError

__all__ = [
"BackendStateMachine",
Expand All @@ -60,10 +60,6 @@ class MessageAction(StrEnum):
RECEIVE = "receive"


class StateMachineError(ProtocolError):
"""Raised when an invalid message is sent/received for the current state."""


class _Transition:
"""Transition to a fixed phase."""

Expand Down
14 changes: 7 additions & 7 deletions tests/unit/test_codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from pygwire.connection import BackendConnection, FrontendConnection
from pygwire.constants import ProtocolVersion, TransactionStatus
from pygwire.exceptions import ProtocolError
from pygwire.exceptions import FramingError
from pygwire.messages import (
AuthenticationOk,
AuthenticationSASL,
Expand Down Expand Up @@ -292,16 +292,16 @@ def test_startup_transitions_to_standard_after_startup_message(self):
assert isinstance(msgs[0], Query)

def test_startup_message_too_short_raises_error(self):
"""Test that short startup message raises ProtocolError."""
"""Test that short startup message raises FramingError."""
conn = BackendConnection()
with pytest.raises(ProtocolError):
with pytest.raises(FramingError):
list(conn.receive(b"\x00\x00\x00\x06\x00\x01"))

def test_unknown_startup_version_raises_error(self):
"""Test unknown startup version code raises ProtocolError."""
"""Test unknown startup version code raises FramingError."""
conn = BackendConnection()
wire = b"\x00\x00\x00\x08\xff\xff\xff\xff"
with pytest.raises(ProtocolError):
with pytest.raises(FramingError):
list(conn.receive(wire))


Expand Down Expand Up @@ -416,14 +416,14 @@ def test_unknown_backend_message_identifier(self):
"""Test that unknown backend message identifier raises error."""
conn = FrontendConnection(initial_phase=ConnectionPhase.READY, strict=False)
wire = b"x\x00\x00\x00\x04"
with pytest.raises(ProtocolError):
with pytest.raises(FramingError):
list(conn.receive(wire))

def test_unknown_frontend_message_identifier(self):
"""Test that unknown frontend message identifier raises error."""
conn = BackendConnection(initial_phase=ConnectionPhase.READY, strict=False)
wire = b"Z\x00\x00\x00\x05I" # 'Z' is backend ReadyForQuery, not a frontend message
with pytest.raises(ProtocolError):
with pytest.raises(FramingError):
list(conn.receive(wire))


Expand Down
Loading
Loading