Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
193 lines
5.9 KiB
Python
193 lines
5.9 KiB
Python
"""CFFI bindings to ``libquicprochat_ffi`` (the Rust C FFI layer).
|
|
|
|
This module loads the shared library and exposes a synchronous Python API
|
|
that mirrors the C functions in ``crates/quicprochat-ffi/src/lib.rs``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import cffi
|
|
|
|
from quicprochat.types import (
|
|
QpqError,
|
|
AuthError,
|
|
TimeoutError,
|
|
ConnectionError,
|
|
)
|
|
|
|
# Status codes (must match crates/quicprochat-ffi/src/lib.rs).
|
|
QPQ_OK = 0
|
|
QPQ_ERROR = 1
|
|
QPQ_AUTH_FAILED = 2
|
|
QPQ_TIMEOUT = 3
|
|
QPQ_NOT_CONNECTED = 4
|
|
|
|
_CDEFS = """
|
|
typedef struct QpqHandle QpqHandle;
|
|
|
|
QpqHandle* qpq_connect(const char* server, const char* ca_cert, const char* server_name);
|
|
int qpq_login(QpqHandle* handle, const char* username, const char* password);
|
|
int qpq_send(QpqHandle* handle, const char* recipient, const uint8_t* message, size_t message_len);
|
|
int qpq_receive(QpqHandle* handle, uint32_t timeout_ms, char** out_json);
|
|
void qpq_disconnect(QpqHandle* handle);
|
|
const char* qpq_last_error(const QpqHandle* handle);
|
|
void qpq_free_string(char* ptr);
|
|
"""
|
|
|
|
_ffi = cffi.FFI()
|
|
_ffi.cdef(_CDEFS)
|
|
_lib: Optional[object] = None
|
|
|
|
|
|
def _load_lib() -> object:
|
|
"""Load the shared library, searching common paths."""
|
|
global _lib
|
|
if _lib is not None:
|
|
return _lib
|
|
|
|
search_paths = [
|
|
# Explicit environment variable.
|
|
os.environ.get("QPQ_LIB_PATH", ""),
|
|
# Common cargo build output locations.
|
|
str(Path(__file__).resolve().parents[3] / "target" / "release" / "libquicprochat_ffi.so"),
|
|
str(Path(__file__).resolve().parents[3] / "target" / "debug" / "libquicprochat_ffi.so"),
|
|
# macOS dylib.
|
|
str(
|
|
Path(__file__).resolve().parents[3]
|
|
/ "target"
|
|
/ "release"
|
|
/ "libquicprochat_ffi.dylib"
|
|
),
|
|
str(
|
|
Path(__file__).resolve().parents[3]
|
|
/ "target"
|
|
/ "debug"
|
|
/ "libquicprochat_ffi.dylib"
|
|
),
|
|
# System library path.
|
|
"libquicprochat_ffi.so",
|
|
]
|
|
|
|
for path in search_paths:
|
|
if not path:
|
|
continue
|
|
try:
|
|
_lib = _ffi.dlopen(path)
|
|
return _lib
|
|
except OSError:
|
|
continue
|
|
|
|
raise OSError(
|
|
"Could not find libquicprochat_ffi. Set QPQ_LIB_PATH or build with "
|
|
"`cargo build --release -p quicprochat-ffi`."
|
|
)
|
|
|
|
|
|
def _check_error(handle: object, code: int) -> None:
|
|
"""Raise an appropriate exception if the FFI call returned an error code."""
|
|
if code == QPQ_OK:
|
|
return
|
|
|
|
lib = _load_lib()
|
|
err_ptr = lib.qpq_last_error(handle) # type: ignore[union-attr]
|
|
msg = _ffi.string(err_ptr).decode("utf-8") if err_ptr != _ffi.NULL else "unknown error"
|
|
|
|
if code == QPQ_AUTH_FAILED:
|
|
raise AuthError(msg)
|
|
if code == QPQ_TIMEOUT:
|
|
raise TimeoutError(msg)
|
|
if code == QPQ_NOT_CONNECTED:
|
|
raise ConnectionError(msg)
|
|
raise QpqError(msg)
|
|
|
|
|
|
class FfiTransport:
|
|
"""Synchronous transport wrapping ``libquicprochat_ffi``.
|
|
|
|
Provides the same logical operations as ``QuicTransport`` but backed
|
|
by the Rust client library through C FFI.
|
|
|
|
Usage::
|
|
|
|
ffi = FfiTransport.connect("127.0.0.1:5001", ca_cert="/path/to/ca.pem")
|
|
ffi.login("alice", "password123")
|
|
ffi.send("bob", b"hello")
|
|
messages = ffi.receive(timeout_ms=5000)
|
|
ffi.close()
|
|
"""
|
|
|
|
def __init__(self, handle: object) -> None:
|
|
self._handle = handle
|
|
self._lib = _load_lib()
|
|
|
|
@staticmethod
|
|
def connect(
|
|
addr: str,
|
|
*,
|
|
ca_cert_path: str = "",
|
|
server_name: str = "",
|
|
) -> "FfiTransport":
|
|
"""Connect to a qpq server via the Rust FFI layer."""
|
|
lib = _load_lib()
|
|
|
|
server_c = _ffi.new("char[]", addr.encode("utf-8"))
|
|
ca_c = _ffi.new("char[]", ca_cert_path.encode("utf-8"))
|
|
|
|
if not server_name:
|
|
host = addr.split(":")[0]
|
|
server_name = host
|
|
sn_c = _ffi.new("char[]", server_name.encode("utf-8"))
|
|
|
|
handle = lib.qpq_connect(server_c, ca_c, sn_c) # type: ignore[union-attr]
|
|
if handle == _ffi.NULL:
|
|
raise ConnectionError(f"qpq_connect failed for {addr}")
|
|
|
|
return FfiTransport(handle)
|
|
|
|
def login(self, username: str, password: str) -> None:
|
|
"""Authenticate with OPAQUE credentials."""
|
|
u = _ffi.new("char[]", username.encode("utf-8"))
|
|
p = _ffi.new("char[]", password.encode("utf-8"))
|
|
code = self._lib.qpq_login(self._handle, u, p) # type: ignore[union-attr]
|
|
_check_error(self._handle, code)
|
|
|
|
def send(self, recipient: str, message: bytes) -> None:
|
|
"""Send a message to a recipient (by username)."""
|
|
r = _ffi.new("char[]", recipient.encode("utf-8"))
|
|
m = _ffi.new("uint8_t[]", message)
|
|
code = self._lib.qpq_send(self._handle, r, m, len(message)) # type: ignore[union-attr]
|
|
_check_error(self._handle, code)
|
|
|
|
def receive(self, timeout_ms: int = 5000) -> list[str]:
|
|
"""Receive pending messages, blocking up to *timeout_ms*.
|
|
|
|
Returns a list of message strings (UTF-8).
|
|
"""
|
|
out = _ffi.new("char**")
|
|
code = self._lib.qpq_receive(self._handle, timeout_ms, out) # type: ignore[union-attr]
|
|
_check_error(self._handle, code)
|
|
|
|
if out[0] == _ffi.NULL:
|
|
return []
|
|
|
|
json_str = _ffi.string(out[0]).decode("utf-8")
|
|
self._lib.qpq_free_string(out[0]) # type: ignore[union-attr]
|
|
return json.loads(json_str) # type: ignore[no-any-return]
|
|
|
|
def close(self) -> None:
|
|
"""Disconnect and free the handle."""
|
|
if self._handle is not None:
|
|
self._lib.qpq_disconnect(self._handle) # type: ignore[union-attr]
|
|
self._handle = None
|
|
|
|
def __enter__(self) -> "FfiTransport":
|
|
return self
|
|
|
|
def __exit__(self, *args: object) -> None:
|
|
self.close()
|