# # Ouroboros - Copyright (C) 2016 - 2026 # # Python API for Ouroboros - Exceptions and errno translation # # Dimitri Staessens # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public License # version 2.1 as published by the Free Software Foundation. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., http://www.fsf.org/about/contact/. # """Exception hierarchy and errno translation for pyouroboros. The hierarchy follows stdlib precedent (``socket``, ``ssl``, ``json``): a single root :class:`OuroborosError`, semantically-named subclasses, and multiple inheritance from Python builtins where the semantics match (e.g. :class:`FlowTimeout` is a :class:`TimeoutError`). """ from __future__ import annotations import errno import os from importlib.metadata import PackageNotFoundError, version # Ouroboros-specific errno values, mirroring include/ouroboros/errno.h. # Imported here (rather than via CFFI) so this module stays FFI-free 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 # --- Root --- class OuroborosError(Exception): """Base class for all pyouroboros exceptions.""" # --- IRM-side --- 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 does not exist.""" class NameExistsError(IrmError): """Raised when a name already exists.""" class InvalidNameError(IrmError, ValueError): """Raised when a name is malformed or otherwise invalid.""" # --- Flow / dev-side --- 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 translation --- # Errno -> Python exception class. Errnos are in their canonical # positive form here; raise_errno() flips the sign. # # Mapped errnos override the caller's `default=` argument, so we only # map errnos whose semantics are unambiguous across both flow and IRM # contexts. EPERM and EINVAL are intentionally NOT mapped: they can # mean very different things on different operations, and a global # mapping would silently override the call-site's chosen default (e.g. # masking IpcpBootstrapError as a bare ValueError). _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) _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", } def raise_errno(rc: int, default: type[OuroborosError] = FlowError) -> int: """Translate a non-negative-or-negative-errno return code. Returns *rc* unchanged when it is non-negative. Otherwise raises the mapped exception (or *default* if no mapping exists). """ 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 *o7s_major*/*o7s_minor* (from ``OUROBOROS_VERSION_{MAJOR,MINOR}`` in the CFFI module) against the installed pyouroboros distribution metadata. Silently returns when running from a source tree without distribution metadata. """ 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]}" )