import asyncio
import functools
import itertools
import logging
import os
import sys
import threading
import time
from copy import deepcopy
try:
import orjson as json
except ImportError:
import json
from ctypes import (
Array,
CDLL,
CFUNCTYPE,
POINTER,
Structure,
addressof,
byref,
cast,
c_char,
c_char_p,
c_int8,
c_int32,
c_int64,
c_ubyte,
c_void_p,
)
from ctypes.util import find_library
from typing import Callable, Optional, Tuple, Union
from weakref import finalize, ref
from ..error import AskarError, AskarErrorCode
LOGGER = logging.getLogger(__name__)
MODULE_NAME = __name__.split(".")[0]
LOG_LEVELS = {
1: logging.ERROR,
2: logging.WARNING,
3: logging.INFO,
4: logging.DEBUG,
}
def entry_cache(fn):
@functools.wraps(fn)
def wrapper(self, index: int):
if not hasattr(self, "_ecache"):
setattr(self, "_ecache", {})
cache = self._ecache
ckey = (fn, index)
if ckey in cache:
res = cache[ckey]
else:
res = fn(self, index)
cache[ckey] = res
if isinstance(res, dict):
res = deepcopy(res)
return res
return wrapper
def _convert_log_level(level: Union[str, int, None]):
if level is None or level == "-1":
return -1
else:
if isinstance(level, str):
level = level.upper()
name = logging.getLevelName(level)
for k, v in LOG_LEVELS.items():
if logging.getLevelName(v) == name:
return k
return 0
def _load_method_arguments(name, argtypes, args):
if not argtypes:
return args
if len(args) != len(argtypes):
raise ValueError(f"{name}: Arguments length does not match argtypes length")
return [
arg if hasattr(argtype, "_type_") else argtype.from_param(arg)
for (arg, argtype) in zip(args, argtypes)
]
def _struct_dtor(ctype: type, address: int, dtor: Callable):
value = ctype.from_address(address)
if value:
dtor(value)
def finalize_struct(instance, ctype):
finalize(
instance, _struct_dtor, ctype, addressof(instance), instance.__class__._cleanup
)
def keepalive(instance, *depend):
finalize(instance, lambda *_args: None, *depend)
class LibLoad:
def __init__(self, lib_name: str):
self._cdll = None
self._callbacks = {}
self._cb_id = itertools.count(0)
self._cfuncs = {}
self._lib_name = lib_name
self._log_cb = None
self._log_enabled_cb = None
self._methods = {}
self._load_library()
self._init_logger()
def _load_library(self):
lib_name = self._lib_name
lib_prefix_mapping = {"win32": ""}
lib_suffix_mapping = {"darwin": ".dylib", "win32": ".dll"}
try:
os_name = sys.platform
lib_prefix = lib_prefix_mapping.get(os_name, "lib")
lib_suffix = lib_suffix_mapping.get(os_name, ".so")
lib_path = os.path.join(
os.path.dirname(__file__), "..", f"{lib_prefix}{lib_name}{lib_suffix}"
)
self._cdll = CDLL(lib_path)
return
except KeyError:
LOGGER.debug("Unknown platform for shared library")
except OSError:
LOGGER.warning("Library not loaded from python package")
lib_path = find_library(lib_name)
if not lib_path:
raise AskarError(
AskarErrorCode.WRAPPER, f"Library not found in path: {lib_name}"
)
try:
self._cdll = CDLL(lib_path)
except OSError as e:
raise AskarError(
AskarErrorCode.WRAPPER, f"Error loading library: {lib_path}"
) from e
def _init_logger(self):
if self._log_cb:
return
logger = logging.getLogger(MODULE_NAME)
if logging.getLevelName("TRACE") == "Level TRACE":
logging.addLevelName(5, "TRACE")
self._log_cb_t = CFUNCTYPE(
None, c_void_p, c_int32, c_char_p, c_char_p, c_char_p, c_char_p, c_int32
)
def _log_cb(
_context,
level: int,
target: c_char_p,
message: c_char_p,
_module_path: c_char_p,
file_name: c_char_p,
line: int,
):
logger.getChild("native." + target.decode().replace("::", ".")).log(
LOG_LEVELS.get(level, level),
"\t%s:%d | %s",
file_name.decode() if file_name else None,
line,
message.decode(),
)
self._log_cb = self._log_cb_t(_log_cb)
self._log_enabled_cb_t = CFUNCTYPE(c_int8, c_void_p, c_int32)
def _enabled_cb(_context, level: int) -> bool:
return self._cdll and logger.isEnabledFor(LOG_LEVELS.get(level, level))
self._log_enabled_cb = self._log_enabled_cb_t(_enabled_cb)
if os.getenv("RUST_LOG"):
level = -1
else:
level = _convert_log_level(logger.level or logger.parent.level)
set_logger = self.method(
"askar_set_custom_logger",
(c_void_p, c_void_p, c_void_p, c_void_p, c_int32),
restype=c_int64,
)
if set_logger(
None, self._log_cb,
self._log_enabled_cb,
None, level,
):
raise self.get_current_error(True)
try:
finalize(self, self.method("askar_clear_custom_logger", None, restype=None))
except AttributeError:
pass
def invoke(self, name, argtypes, *args):
method = self.method(name, argtypes, restype=c_int64)
if not method:
raise ValueError(f"FFI method not found: {name}")
args = _load_method_arguments(name, argtypes, args)
result = method(*args)
if result:
raise self.get_current_error(True)
def invoke_async(
self, name: str, argtypes, *args, return_type=None
) -> asyncio.Future:
method = self.method(name, (*argtypes, c_void_p, c_int64), restype=c_int64)
if not method:
raise ValueError(f"FFI method not found: {name}")
loop = asyncio.get_event_loop()
fut = loop.create_future()
cb_info = self._cfuncs.get(name)
if cb_info:
cb = cb_info[1]
else:
cb_args = [c_int64, c_int64]
if return_type:
cb_args.append(return_type)
cb_type = CFUNCTYPE(None, *cb_args)
cb = cb_type(self._handle_callback)
self._cfuncs[name] = (cb_type, cb)
args = _load_method_arguments(name, argtypes, args)
cb_id = next(self._cb_id)
self._callbacks[cb_id] = (loop, fut, name)
result = method(*args, cb, cb_id)
if result:
err = self.get_current_error(True)
if self._callbacks.pop(cb_id, None):
self._fulfill_future(fut, None, err)
return fut
def invoke_dtor(self, name: str, *values, argtypes=None, restype=None):
method = self.method(name, argtypes, restype=restype)
if method:
method(*values)
def _handle_callback(self, cb_id: int, err: int, result=None):
exc = self.get_current_error(True) if err else None
cb = self._callbacks.pop(cb_id, None)
if not cb:
LOGGER.info("Callback already fulfilled: %s", cb_id)
return
(loop, fut, _name) = cb
loop.call_soon_threadsafe(self._fulfill_future, fut, result, exc)
def _fulfill_future(self, fut: asyncio.Future, result, err: Exception = None):
if fut.cancelled():
LOGGER.debug("callback previously cancelled")
elif err:
fut.set_exception(err)
else:
fut.set_result(result)
def get_current_error(self, expect: bool = False) -> Optional[AskarError]:
err_json = StrBuffer()
method = self.method(
"askar_get_current_error", (POINTER(StrBuffer),), restype=c_int64
)
if not method(byref(err_json)):
try:
msg = json.loads(err_json.value)
except json.JSONDecodeError:
LOGGER.warning("JSON decode error for askar_get_current_error")
msg = None
if msg and "message" in msg and "code" in msg:
return AskarError(
AskarErrorCode(msg["code"]), msg["message"], msg.get("extra")
)
if not expect:
return None
return AskarError(AskarErrorCode.WRAPPER, "Unknown error")
def method(self, name, argtypes, *, restype=None):
method = self._methods.get(name)
if not method:
method = getattr(self._cdll, name, None)
if not method:
return None
if argtypes:
method.argtypes = argtypes
method.restype = restype
self._methods[name] = method
return method
def _cleanup(self):
if self._callbacks:
def _wait_callbacks(cb):
while cb:
time.sleep(0.01)
th = threading.Thread(target=_wait_callbacks, args=(self._callbacks,))
th.start()
th.join(timeout=1.0)
if th.is_alive():
LOGGER.error(
"%s: Timed out waiting for callbacks to complete",
self._lib_name,
)
self.method("askar_terminate", None, restype=None)()
class Lib:
INSTANCE = None
LIB_NAME = "aries_askar"
def __new__(cls, *args):
inst = cls.INSTANCE and cls.INSTANCE()
if inst is None:
inst = super().__new__(cls, *args)
inst._initlock = threading.Lock()
inst._lib = None
inst._objs = []
cls.INSTANCE = ref(inst)
finalize(inst, cls._cleanup, inst._objs)
return inst
@property
def loaded(self) -> LibLoad:
if not self._lib:
with self._initlock:
if not self._lib:
self._lib = LibLoad(self.__class__.LIB_NAME)
self._objs.append(self._lib)
return self._lib
def invoke(self, name, argtypes, *args):
self.loaded.invoke(name, argtypes, *args)
async def invoke_async(self, name: str, argtypes, *args, return_type=None):
return await self.loaded.invoke_async(
name, argtypes, *args, return_type=return_type
)
def invoke_dtor(self, name: str, *args, argtypes=None, restype=None):
if self._lib:
self._lib.invoke_dtor(name, *args, argtypes=argtypes, restype=restype)
def set_max_log_level(self, level: Union[str, int, None]):
set_level = _convert_log_level(level)
self.invoke("askar_set_max_log_level", (c_int32,), set_level)
def version(self) -> str:
return str(
self.loaded.method(
"askar_version",
None,
restype=StrBuffer,
)()
)
def __repr__(self) -> str:
loaded = self._lib is not None
return f"<Lib('{self.__class__.LIB_NAME}', loaded={loaded})>"
@classmethod
def _cleanup(cls, objs):
for obj in objs:
obj._cleanup()
class RawBuffer(Structure):
_fields_ = [
("len", c_int64),
("data", POINTER(c_ubyte)),
]
def __bool__(self) -> bool:
return bool(self.data)
def __bytes__(self) -> bytes:
if not self.len:
return b""
return bytes(self.array)
def __len__(self) -> int:
return self.len.value
@property
def array(self) -> Array:
return cast(self.data, POINTER(c_ubyte * self.len)).contents
def __repr__(self) -> str:
return f"<RawBuffer(len={self.len})>"
class FfiByteBuffer:
def __init__(self, value):
if isinstance(value, str):
value = value.encode("utf-8")
if value is None:
dlen = 0
data = c_char_p()
elif isinstance(value, memoryview):
dlen = value.nbytes
data = c_char_p(value.tobytes())
elif isinstance(value, bytes):
dlen = len(value)
data = c_char_p(value)
else:
raise TypeError(f"Expected str or bytes value, got {type(value)}")
self._dlen = dlen
self._data = data
def __bytes__(self) -> bytes:
if not self._data:
return b""
return self._data.value
def __len__(self) -> int:
return self._dlen
@property
def _as_parameter_(self) -> RawBuffer:
buf = RawBuffer(len=self._dlen, data=cast(self._data, POINTER(c_ubyte)))
return buf
@classmethod
def from_param(cls, value):
if isinstance(value, (ByteBuffer, FfiByteBuffer)):
return value
return cls(value)
class ByteBuffer(Structure):
_fields_ = [("buffer", RawBuffer)]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
finalize_struct(self, RawBuffer)
@property
def _as_parameter_(self):
return self.buffer
@property
def array(self) -> Array:
return self.buffer.array
@property
def view(self) -> memoryview:
m = memoryview(self.array)
keepalive(m, self)
return m
def __bytes__(self) -> bytes:
return bytes(self.buffer)
def __len__(self) -> int:
return len(self.buffer)
def __getitem__(self, idx) -> bytes:
return bytes(self.buffer.array[idx])
def __repr__(self) -> str:
return f"{self.__class__.__name__}({bytes(self)})"
@classmethod
def _cleanup(cls, buffer: RawBuffer):
Lib().invoke_dtor("askar_buffer_free", buffer)
class FfiStr:
def __init__(self, value=None):
if value is None:
value = c_char_p()
elif isinstance(value, c_char_p):
pass
else:
if isinstance(value, str):
value = value.encode("utf-8")
if not isinstance(value, bytes):
raise TypeError(f"Expected string value, got {type(value)}")
value = c_char_p(value)
self.value = value
@classmethod
def from_param(cls, value):
if isinstance(value, cls):
return value
return cls(value)
@property
def _as_parameter_(self):
return self.value
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.value})"
class FfiJson:
@classmethod
def from_param(cls, value):
if isinstance(value, FfiStr):
return value
if isinstance(value, dict):
value = json.dumps(value)
return FfiStr(value)
class FfiTagsJson:
@classmethod
def from_param(cls, tags):
if isinstance(tags, FfiStr):
return tags
if tags:
tags = json.dumps(
{
name: (list(value) if isinstance(value, set) else value)
for name, value in tags.items()
}
)
else:
tags = None
return FfiStr(tags)
class StrBuffer(Structure):
_fields_ = [("buffer", POINTER(c_char))]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
finalize_struct(self, c_char_p)
def is_none(self) -> bool:
return not self.buffer
def opt_str(self) -> Optional[str]:
val = self.value
return val.decode("utf-8") if val is not None else None
def __bool__(self) -> bool:
return bool(self.buffer)
def __bytes__(self) -> bytes:
bval = self.value
return bval if bval is not None else bytes()
def __str__(self):
val = self.opt_str()
return val if val is not None else ""
@property
def value(self) -> bytes:
return cast(self.buffer, c_char_p).value
@classmethod
def _cleanup(cls, buffer: c_char_p):
Lib().invoke_dtor("askar_string_free", buffer)
class AeadParams(Structure):
_fields_ = [
("nonce_length", c_int32),
("tag_length", c_int32),
]
def __repr__(self) -> str:
return (
f"<AeadParams(nonce_length={self.nonce_length}, "
f"tag_length={self.tag_length})>"
)
class Encrypted(Structure):
_fields_ = [
("buffer", RawBuffer),
("tag_pos", c_int64),
("nonce_pos", c_int64),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
finalize_struct(self, RawBuffer)
def __getitem__(self, idx) -> bytes:
return bytes(self.buffer.array[idx])
def __bytes__(self) -> bytes:
return self.ciphertext_tag
@property
def ciphertext_tag(self) -> bytes:
p = self.nonce_pos
return self[:p]
@property
def ciphertext(self) -> bytes:
p = self.tag_pos
return self[:p]
@property
def nonce(self) -> bytes:
p = self.nonce_pos
return self[p:]
@property
def tag(self) -> bytes:
p1 = self.tag_pos
p2 = self.nonce_pos
return self[p1:p2]
@property
def parts(self) -> Tuple[bytes, bytes, bytes]:
p1 = self.tag_pos
p2 = self.nonce_pos
return self[:p1], self[p1:p2], self[p2:]
def __repr__(self) -> str:
return (
f"<Encrypted(ciphertext={self.ciphertext}, tag={self.tag},"
f" nonce={self.nonce})>"
)
@classmethod
def _cleanup(cls, buffer: RawBuffer):
Lib().invoke_dtor("askar_buffer_free", buffer)