opa-golib-python-bindings 0.4.3__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/Makefile +5 -1
  2. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/PKG-INFO +2 -1
  3. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/pyproject.toml +3 -2
  4. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/src/opa_bindings/engine.py +45 -14
  5. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/.gitignore +0 -0
  6. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/LICENSE +0 -0
  7. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/README.md +0 -0
  8. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/bridge.go +0 -0
  9. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/bridge_call.c +0 -0
  10. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/builtins.go +0 -0
  11. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/compile.go +0 -0
  12. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/go.mod +0 -0
  13. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/go.sum +0 -0
  14. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/merge.go +0 -0
  15. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/go/trace.go +0 -0
  16. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/hatch_build.py +0 -0
  17. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/src/opa_bindings/__init__.py +0 -0
  18. {opa_golib_python_bindings-0.4.3 → opa_golib_python_bindings-0.4.4}/src/opa_bindings/_native.py +0 -0
@@ -1,6 +1,6 @@
1
1
  LIB := src/opa_bindings/libopabridge.so
2
2
 
3
- .PHONY: all build test clean
3
+ .PHONY: all build test fuzz clean
4
4
 
5
5
  all: build
6
6
 
@@ -10,5 +10,9 @@ build:
10
10
  test: build
11
11
  python -m pytest tests/ -v
12
12
 
13
+ FUZZ_EXAMPLES ?= 2000
14
+ fuzz: build
15
+ PYTHONFAULTHANDLER=1 FUZZ_EXAMPLES=$(FUZZ_EXAMPLES) python -m pytest tests/test_fuzz_bindings.py -m fuzz -v
16
+
13
17
  clean:
14
18
  rm -f $(LIB) src/opa_bindings/libopabridge.h
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opa-golib-python-bindings
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: Python bindings for the OPA (Open Policy Agent) Rego engine via a Go c-shared library
5
5
  Project-URL: Homepage, https://github.com/phi1010/opa-golib-python-bindings
6
6
  Project-URL: Repository, https://github.com/phi1010/opa-golib-python-bindings
@@ -20,6 +20,7 @@ Classifier: Topic :: Security
20
20
  Classifier: Topic :: Software Development :: Libraries
21
21
  Requires-Python: >=3.14
22
22
  Provides-Extra: dev
23
+ Requires-Dist: hypothesis; extra == 'dev'
23
24
  Requires-Dist: pytest; extra == 'dev'
24
25
  Requires-Dist: pyyaml; extra == 'dev'
25
26
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opa-golib-python-bindings"
3
- version = "0.4.3"
3
+ version = "0.4.4"
4
4
  description = "Python bindings for the OPA (Open Policy Agent) Rego engine via a Go c-shared library"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -26,7 +26,7 @@ Repository = "https://github.com/phi1010/opa-golib-python-bindings"
26
26
  Issues = "https://github.com/phi1010/opa-golib-python-bindings/issues"
27
27
 
28
28
  [project.optional-dependencies]
29
- dev = ["pytest", "pyyaml"]
29
+ dev = ["pytest", "pyyaml", "hypothesis"]
30
30
 
31
31
  [build-system]
32
32
  requires = ["hatchling"]
@@ -53,3 +53,4 @@ include = [
53
53
 
54
54
  [tool.pytest.ini_options]
55
55
  pythonpath = ["src"]
56
+ markers = ["fuzz: hypothesis fuzz tests of the Python/Go binding layer"]
@@ -129,7 +129,12 @@ class OpaEngine:
129
129
  raise OpaError("closed", "engine has been closed")
130
130
 
131
131
  def _call(self, fn, *args):
132
- ptr = fn(self._handle, *args)
132
+ # Read the handle once: another thread may close() the engine after
133
+ # _check_open(). A stale handle is rejected by the Go registry.
134
+ handle = self._handle
135
+ if handle is None:
136
+ raise OpaError("closed", "engine has been closed")
137
+ ptr = fn(handle, *args)
133
138
  if not ptr:
134
139
  raise OpaError("internal", "native call returned NULL")
135
140
  try:
@@ -174,7 +179,7 @@ class OpaEngine:
174
179
  except Exception as e: # never let an exception cross into Go
175
180
  response = {"error": repr(e)}
176
181
  try:
177
- raw = json.dumps(response).encode()
182
+ raw = _dumps(response)
178
183
  except Exception as e:
179
184
  raw = json.dumps({"error": f"unserializable result: {e!r}"}).encode()
180
185
  # malloc with the C allocator: ownership of this buffer transfers to
@@ -194,7 +199,7 @@ class OpaEngine:
194
199
  def add_policy(self, path: str, source: str):
195
200
  """Add a Rego module under the given path (module name)."""
196
201
  self._check_open()
197
- self._call(self._lib.OpaAddPolicy, path.encode(), source.encode())
202
+ self._call(self._lib.OpaAddPolicy, _cstr(path), _cstr(source))
198
203
 
199
204
  def add_data(self, value, path: str = ""):
200
205
  """Deep-merge a JSON-compatible value into the data document.
@@ -203,7 +208,7 @@ class OpaEngine:
203
208
  Conflicting existing values raise OpaError(code="merge_conflict").
204
209
  """
205
210
  self._check_open()
206
- self._call(self._lib.OpaAddData, path.encode(), json.dumps(value).encode())
211
+ self._call(self._lib.OpaAddData, _cstr(path), _dumps(value))
207
212
 
208
213
  def register_function(self, name: str, fn, *, arity: int | None = None):
209
214
  """Expose a Python callable as a Rego builtin.
@@ -228,7 +233,7 @@ class OpaEngine:
228
233
  self._functions[name] = (fn, arity)
229
234
  try:
230
235
  self._call(
231
- self._lib.OpaRegisterBuiltin, name.encode(), arity, self._trampoline
236
+ self._lib.OpaRegisterBuiltin, _cstr(name), arity, self._trampoline
232
237
  )
233
238
  except Exception:
234
239
  del self._functions[name]
@@ -250,7 +255,7 @@ class OpaEngine:
250
255
  rs = self._eval_result(
251
256
  self._call(
252
257
  self._lib.OpaEvalQuery,
253
- query.encode(),
258
+ _cstr(query),
254
259
  _encode_input(input),
255
260
  int(coverage),
256
261
  int(trace),
@@ -275,7 +280,7 @@ class OpaEngine:
275
280
  rs = self._eval_result(
276
281
  self._call(
277
282
  self._lib.OpaEvalDocument,
278
- path.encode(),
283
+ _cstr(path),
279
284
  _encode_input(input),
280
285
  int(coverage),
281
286
  int(trace),
@@ -332,18 +337,44 @@ class OpaEngine:
332
337
  with self._eval_lock:
333
338
  envelope = self._call(
334
339
  self._lib.OpaCompileFilters,
335
- query.encode(),
340
+ _cstr(query),
336
341
  _encode_input(input),
337
- json.dumps(list(unknowns)).encode(),
338
- target.encode(),
339
- dialect.encode(),
340
- b"" if mappings is None else json.dumps(mappings).encode(),
341
- (mask_rule or "").encode(),
342
+ _dumps(list(unknowns)),
343
+ _cstr(target),
344
+ _cstr(dialect),
345
+ b"" if mappings is None else _dumps(mappings),
346
+ _cstr(mask_rule or ""),
342
347
  )
343
348
  return envelope.get("result")
344
349
 
345
350
 
351
+ def _cstr(s: str) -> bytes:
352
+ """Encode a string for a char* argument.
353
+
354
+ Go reads C strings up to the first NUL, so an embedded NUL would silently
355
+ truncate the string (e.g. ``"a\\0b"`` aliasing ``"a"``). Lone surrogates
356
+ are not valid UTF-8 and would be replaced with U+FFFD, letting distinct
357
+ strings collide. Both are rejected.
358
+ """
359
+ if "\0" in s:
360
+ raise OpaError("invalid_argument", "string contains a NUL character")
361
+ try:
362
+ return s.encode("utf-8")
363
+ except UnicodeEncodeError as e:
364
+ raise OpaError("invalid_argument", f"string is not valid UTF-8: {e}") from None
365
+
366
+
367
+ def _dumps(value) -> bytes:
368
+ """Serialize JSON for the Go side. NULs are escaped by json; encoding
369
+ without ensure_ascii makes lone surrogates (which Go would replace with
370
+ U+FFFD) fail instead of being escaped."""
371
+ try:
372
+ return json.dumps(value, ensure_ascii=False).encode("utf-8")
373
+ except UnicodeEncodeError as e:
374
+ raise OpaError("invalid_argument", f"value is not valid UTF-8: {e}") from None
375
+
376
+
346
377
  def _encode_input(input):
347
378
  if input is None:
348
379
  return b""
349
- return json.dumps(input).encode()
380
+ return _dumps(input)