from __future__ import annotations
import ctypes
import ctypes.util
import json
import os
import platform
import sys
from pathlib import Path
from typing import Any, Mapping
__all__ = [
"EnprotError",
"version",
"process",
"encrypt",
"decrypt",
"store",
"fetch",
]
__version__ = "0.1.0"
ENPROT_OK = 0
ENPROT_ERR_PARSE = 1
ENPROT_ERR_CRYPTO = 2
ENPROT_ERR_IO = 3
ENPROT_ERR_INVALID = 4
_ERROR_NAMES = {
ENPROT_ERR_PARSE: "parse",
ENPROT_ERR_CRYPTO: "crypto",
ENPROT_ERR_IO: "io",
ENPROT_ERR_INVALID: "invalid",
}
class EnprotError(Exception):
def __init__(self, code: int, message: str) -> None:
self.code = code
self.category = _ERROR_NAMES.get(code, "unknown")
super().__init__(f"[enprot {self.category}] {message}")
class _EnprotResult(ctypes.Structure):
_fields_ = [
("code", ctypes.c_int),
("error", ctypes.c_void_p),
]
def _lib_name() -> str:
system = platform.system()
if system == "Windows":
return "enprot.dll"
if system == "Darwin":
return "libenprot.dylib"
return "libenprot.so"
def _find_library() -> ctypes.CDLL:
candidates: list[Path] = []
env = os.environ.get("ENPROT_LIB")
if env:
candidates.append(Path(env))
here = Path(__file__).resolve().parent
repo_root = here.parents[2]
candidates.append(repo_root / "target" / "release" / _lib_name())
candidates.append(repo_root / "target" / "debug" / _lib_name())
for c in candidates:
if c.is_file():
return ctypes.CDLL(str(c))
found = ctypes.util.find_library("enprot")
if found:
return ctypes.CDLL(found)
searched = ", ".join(str(c) for c in candidates)
raise ImportError(
f"could not locate {_lib_name()}. Set ENPROT_LIB or build the "
f"enprot crate with `cargo build --release` (cdylib). Searched: {searched}"
)
_lib = _find_library()
_lib.enprot_process.restype = _EnprotResult
_lib.enprot_process.argtypes = [ctypes.c_char_p]
_lib.enprot_version.restype = ctypes.c_char_p
_lib.enprot_version.argtypes = []
_lib.enprot_free_error.restype = None
_lib.enprot_free_error.argtypes = [ctypes.c_void_p]
def version() -> str:
return ctypes.cast(_lib.enprot_version(), ctypes.c_char_p).value.decode("utf-8")
def process(config: Mapping[str, Any]) -> None:
payload = json.dumps(config).encode("utf-8")
result = _lib.enprot_process(payload)
if result.code != ENPROT_OK:
msg = "(no message)"
if result.error:
try:
raw = ctypes.cast(result.error, ctypes.c_char_p).value
msg = raw.decode("utf-8") if raw else msg
finally:
_lib.enprot_free_error(result.error)
raise EnprotError(result.code, msg)
def _run(operation: str, file: str | os.PathLike[str], **kwargs: Any) -> None:
config: dict[str, Any] = {"operation": operation, "file": str(file)}
config.update(kwargs)
process(config)
def encrypt(
file: str | os.PathLike[str],
*,
words: Mapping[str, str] | None = None,
cipher: str | None = None,
casdir: str | os.PathLike[str] | None = None,
policy: str | None = None,
) -> None:
_run(
"encrypt",
file,
words=dict(words) if words else None,
cipher=cipher,
casdir=str(casdir) if casdir is not None else None,
policy=policy,
)
def decrypt(
file: str | os.PathLike[str],
*,
words: Mapping[str, str] | None = None,
casdir: str | os.PathLike[str] | None = None,
policy: str | None = None,
) -> None:
_run(
"decrypt",
file,
words=dict(words) if words else None,
casdir=str(casdir) if casdir is not None else None,
policy=policy,
)
def store(
file: str | os.PathLike[str],
*,
words: Mapping[str, str] | None = None,
casdir: str | os.PathLike[str] | None = None,
) -> None:
_run(
"store",
file,
words=dict(words) if words else None,
casdir=str(casdir) if casdir is not None else None,
)
def fetch(
file: str | os.PathLike[str],
*,
words: Mapping[str, str] | None = None,
casdir: str | os.PathLike[str] | None = None,
) -> None:
_run(
"fetch",
file,
words=dict(words) if words else None,
casdir=str(casdir) if casdir is not None else None,
)