diff options
Diffstat (limited to 'ouroboros/errors.py')
| -rw-r--r-- | ouroboros/errors.py | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/ouroboros/errors.py b/ouroboros/errors.py new file mode 100644 index 0000000..6ab6053 --- /dev/null +++ b/ouroboros/errors.py @@ -0,0 +1,284 @@ +# +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only +# + +""" +Exception hierarchy and errno translation for pyouroboros. + +The ``E*`` constants mirror the Ouroboros-specific errno values in +``include/ouroboros/errno.h``. +""" + +from __future__ import annotations + +import errno +import os +from importlib.metadata import PackageNotFoundError, version + +ENOTALLOC = 1000 # Flow is not allocated +EIPCPTYPE = 1001 # Unknown IPCP type +EIRMD = 1002 # Failed to communicate with IRMD +EIPCP = 1003 # Failed to communicate with IPCP +EIPCPSTATE = 1004 # Target in wrong state +EFLOWDOWN = 1005 # Flow is down +EFLOWPEER = 1006 # Flow is down (peer timed out) +ENAME = 1007 # Naming error +ECRYPT = 1008 # Encryption error +EAUTH = 1009 # Authentication error +EREPLAY = 1010 # OAP replay detected + +_O7S_ERRNO_STR: dict[int, str] = { + ENOTALLOC: "flow is not allocated", + EIPCPTYPE: "unknown IPCP type", + EIRMD: "failed to communicate with IRMd", + EIPCP: "failed to communicate with IPCP", + EIPCPSTATE: "target in wrong state", + EFLOWDOWN: "flow is down", + EFLOWPEER: "flow peer timed out", + ENAME: "naming error", + ECRYPT: "encryption error", + EAUTH: "authentication error", + EREPLAY: "OAP replay detected", +} + + +class OuroborosError(Exception): + """ + Base class for all pyouroboros exceptions. + """ + + +class IrmError(OuroborosError): + """ + Base class for IRM (control plane) errors. + """ + + +class IpcpCreateError(IrmError): + """ + Raised when IPCP creation fails. + """ + + +class IpcpBootstrapError(IrmError): + """ + Raised when IPCP bootstrapping fails. + """ + + +class IpcpEnrollError(IrmError): + """ + Raised when IPCP enrollment fails. + """ + + +class IpcpConnectError(IrmError): + """ + Raised when IPCP connection or disconnection fails. + """ + + +class IpcpTypeError(IrmError, ValueError): + """ + Raised when an unknown IPCP type is encountered. + """ + + +class IpcpStateError(IrmError): + """ + Raised when an IPCP/IRMd is in the wrong state for the request. + """ + + +class IrmdError(IrmError): + """ + Raised when the IRM daemon is unreachable. + """ + + +class IpcpdError(IrmError): + """ + Raised when the IPCP daemon is unreachable. + """ + + +class BindError(IrmError): + """ + Raised when binding a program/process/IPCP to a name fails. + """ + + +class NameNotFoundError(IrmError): + """ + Raised when a name lookup or unregister target is missing. + """ + + +class NameExistsError(IrmError): + """ + Raised when a name already exists. + """ + + +class InvalidNameError(IrmError, ValueError): + """ + Raised when a name is malformed or otherwise invalid. + """ + + +class FlowError(OuroborosError): + """ + Base class for flow-plane errors. + """ + + +class FlowAlreadyAllocatedError(FlowError): + """ + Raised when allocating on a Flow that already holds an fd. + """ + + +class FlowNotAllocatedError(FlowError): + """ + Raised when operating on a Flow that has no fd. + """ + + +class FlowDownError(FlowError, ConnectionError): + """ + Raised when the flow has gone down. + """ + + +class FlowPeerError(FlowDownError): + """ + Raised when the flow's peer has timed out. + """ + + +class FlowPermissionError(FlowError, PermissionError): + """ + Raised when an operation is not permitted on the flow. + """ + + +class FlowTimeout(FlowError, TimeoutError): + """ + Raised when a flow operation times out. + """ + + +class FlowCryptError(FlowError): + """ + Raised on flow encryption errors. + """ + + +class FlowAuthError(FlowError): + """ + Raised on flow authentication errors. + """ + + +class FlowReplayError(FlowError): + """ + Raised when OAP replay is detected. + """ + + +class FlowEventError(FlowError): + """ + Raised on errors from the flow event subsystem. + """ + + +class FlowDeallocWarning(Warning): + """ + Warning issued when a deallocation reports a non-fatal problem. + """ + + +_ERRNO_MAP: dict[int, type[BaseException]] = { + errno.ETIMEDOUT: TimeoutError, + errno.EAGAIN: BlockingIOError, + errno.EWOULDBLOCK: BlockingIOError, + errno.ENOMEM: MemoryError, + errno.EACCES: PermissionError, + errno.ENOTCONN: ConnectionError, + errno.ECONNRESET: ConnectionResetError, + ENOTALLOC: FlowNotAllocatedError, + EIPCPTYPE: IpcpTypeError, + EIRMD: IrmdError, + EIPCP: IpcpdError, + EIPCPSTATE: IpcpStateError, + EFLOWDOWN: FlowDownError, + EFLOWPEER: FlowPeerError, + ENAME: NameNotFoundError, + ECRYPT: FlowCryptError, + EAUTH: FlowAuthError, + EREPLAY: FlowReplayError, +} + + +def _strerror(err: int) -> str: + if err >= 1000: + return _O7S_ERRNO_STR.get(err, f"ouroboros errno {err}") + + return os.strerror(err) + + +def raise_errno(rc: int, default: type[OuroborosError] = FlowError) -> int: + """ + Translate a non-negative-or-negative-errno return code. + + ``_ERRNO_MAP`` holds errnos in their canonical positive form. + + :param rc: The return code to translate; a negative + value is a negated errno. + :param default: Exception class to raise when the errno + has no mapping; a mapped errno overrides + it. + :return: *rc* unchanged when it is non-negative. + :raises OuroborosError: (or the builtin mapped for ``-rc``) when + *rc* is negative. + """ + + if rc >= 0: + return rc + + err = -rc + exc_cls = _ERRNO_MAP.get(err, default) + raise exc_cls(_strerror(err)) + + +def check_ouroboros_version(o7s_major: int, o7s_minor: int) -> None: + """ + Verify that the installed pyouroboros matches the linked library. + + Compares the linked library version against the installed + pyouroboros distribution metadata. Silently returns when running + from a source tree without distribution metadata. + + :param o7s_major: Major version of the linked library, from + ``OUROBOROS_VERSION_MAJOR`` in the CFFI + module. + :param o7s_minor: Minor version of the linked library, from + ``OUROBOROS_VERSION_MINOR`` in the CFFI + module. + :raises RuntimeError: If the library and pyouroboros ``major.minor`` + differ. + """ + + try: + pyo7s_parts = version("PyOuroboros").split(".") + except PackageNotFoundError: + return + + if o7s_major != int(pyo7s_parts[0]) or \ + o7s_minor != int(pyo7s_parts[1]): + raise RuntimeError( + f"Ouroboros version mismatch: library is " + f"{o7s_major}.{o7s_minor}, pyouroboros is " + f"{pyo7s_parts[0]}.{pyo7s_parts[1]}" + ) |
