From 15431156b2d867e35e21b022d71385265a5b84a6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 21:55:45 +0000
Subject: [PATCH 1/4] Initial plan
From b9f00295a7d763af2a8c71e93666829015a8450a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 21:58:38 +0000
Subject: [PATCH 2/4] Initial plan: update module to work with upstream
libinjection
Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com>
---
libinjection/libinjection_error.h | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 libinjection/libinjection_error.h
diff --git a/libinjection/libinjection_error.h b/libinjection/libinjection_error.h
new file mode 100644
index 0000000..90c6713
--- /dev/null
+++ b/libinjection/libinjection_error.h
@@ -0,0 +1,26 @@
+/**
+ * LibInjection Project
+ * BSD License -- see `COPYING.txt` for details
+ *
+ * https://github.com/libinjection/libinjection/
+ *
+ */
+
+#ifndef LIBINJECTION_ERROR_H
+#define LIBINJECTION_ERROR_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum injection_result_t {
+ LIBINJECTION_RESULT_FALSE = 0,
+ LIBINJECTION_RESULT_TRUE = 1,
+ LIBINJECTION_RESULT_ERROR = -1
+} injection_result_t;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // LIBINJECTION_ERROR_H
From e551e7c6159b45b726eda6c5c293d3cefaf299e8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 22:11:07 +0000
Subject: [PATCH 3/4] Update module to work with upstream libinjection and
improve Python 3 compatibility
Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com>
---
.gitignore | 4 ++
Makefile | 20 +++----
README.md | 91 +++++++++++++++++++++++++++++++
libinjection/__init__.py | 2 +-
libinjection/libinjection.i | 32 +++++++++--
libinjection/libinjection_error.h | 26 ---------
setup.py | 24 +++++++-
7 files changed, 155 insertions(+), 44 deletions(-)
delete mode 100644 libinjection/libinjection_error.h
diff --git a/.gitignore b/.gitignore
index 3d30f57..c5da1f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -136,3 +136,7 @@ libinjection/libinjection_xss.*
libinjection/libinjection_html5.*
libinjection/libinjection_sqli*
libinjection/libinjection_wrap*
+libinjection/libinjection_error.h
+
+# Generated files
+words.py
diff --git a/Makefile b/Makefile
index f3aeae4..354c23a 100644
--- a/Makefile
+++ b/Makefile
@@ -4,15 +4,14 @@ all: build
#
build: upstream libinjection/libinjection_wrap.c
- rm -f libinjection.py libinjection.pyc
- python setup.py --verbose build --force
+ rm -f libinjection/libinjection.py
+ python3 setup.py --verbose build_ext --inplace
install: build
- sudo python setup.py --verbose install
+ sudo python3 setup.py --verbose install
test-unit: build words.py
- python setup.py build_ext --inplace
- PYTHON_PATH='.' nosetests -v --with-xunit test_driver.py
+ cd /tmp && python3 -m pytest $(CURDIR)/test_driver.py -v
.PHONY: test
test: test-unit
@@ -24,16 +23,16 @@ speed:
upstream:
[ -d $@ ] || git clone --depth=1 https://github.com/libinjection/libinjection.git upstream
-libinjection/libinjection.h libinjection/libinjection_sqli.h: upstream
+libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h: upstream
cp -f upstream/src/libinjection*.h upstream/src/libinjection*.c libinjection/
words.py: Makefile json2python.py upstream
- ./json2python.py < upstream/src/sqlparse_data.json > words.py
+ python3 json2python.py < upstream/src/sqlparse_data.json > words.py
-libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h libinjection/libinjection_sqli.h
+libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h
swig -version
- swig -py3 -python -builtin -Wall -Wextra libinjection/libinjection.i
+ swig -python -builtin -Wall -Wextra libinjection/libinjection.i
.PHONY: copy
@@ -50,5 +49,6 @@ clean:
@rm -f nosetests.xml
@rm -f words.py
@rm -f libinjection/*~ libinjection/*.pyc
- @rm -f libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c libinjection/libinjection_sqli_data.h
+ @rm -f libinjection/libinjection.h libinjection/libinjection_error.h libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c libinjection/libinjection_sqli_data.h
+ @rm -f libinjection/libinjection_html5.h libinjection/libinjection_html5.c libinjection/libinjection_xss.h libinjection/libinjection_xss.c
@rm -f libinjection/libinjection_wrap.c libinjection/libinjection.py
diff --git a/README.md b/README.md
index d2937aa..3d85508 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,93 @@
# python3-libinjection
libInjection Python3 bindings
+
+## Overview
+
+Python3 bindings for [libinjection](https://github.com/libinjection/libinjection) - a SQL/SQLI tokenizer, parser and analyzer.
+
+## Requirements
+
+- Python 3.x
+- SWIG 4.x
+- GCC or compatible C compiler
+
+## Building
+
+### 1. Clone the repository and get upstream libinjection
+
+```bash
+git clone https://github.com/libinjection/python3-libinjection.git
+cd python3-libinjection
+make upstream
+```
+
+### 2. Copy upstream C source files
+
+```bash
+make libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h
+```
+
+### 3. Generate the SWIG wrapper
+
+```bash
+swig -python -builtin -Wall -Wextra libinjection/libinjection.i
+```
+
+### 4. Build the Python extension
+
+```bash
+python3 setup.py build_ext --inplace
+```
+
+Or using the Makefile:
+
+```bash
+make build
+```
+
+### 5. Generate the word lookup table (needed for tests)
+
+```bash
+python3 json2python.py < upstream/src/sqlparse_data.json > words.py
+```
+
+## Usage
+
+### SQLi Detection
+
+```python
+import libinjection
+
+# Simple API - detect SQLi in a string
+result, fingerprint = libinjection.sqli("1 UNION SELECT * FROM users")
+if result:
+ print(f"SQLi detected! Fingerprint: {fingerprint}")
+
+# Advanced API with state object
+state = libinjection.sqli_state()
+libinjection.sqli_init(state, "1 UNION SELECT * FROM users",
+ libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI)
+libinjection.sqli_callback(state, None)
+if libinjection.is_sqli(state):
+ print(f"SQLi detected! Fingerprint: {state.fingerprint}")
+```
+
+### XSS Detection
+
+```python
+import libinjection
+
+# Detect XSS in a string
+result = libinjection.xss("")
+if result:
+ print("XSS detected!")
+```
+
+## Testing
+
+Run the test suite using pytest:
+
+```bash
+cd /tmp # run from outside the repo directory to avoid pytest.py conflict
+python3 -m pytest /path/to/python3-libinjection/test_driver.py -v
+```
diff --git a/libinjection/__init__.py b/libinjection/__init__.py
index 84d587a..f16540d 100644
--- a/libinjection/__init__.py
+++ b/libinjection/__init__.py
@@ -1 +1 @@
-from libinjection import *
+from .libinjection import *
diff --git a/libinjection/libinjection.i b/libinjection/libinjection.i
index 3f279da..8d91dff 100644
--- a/libinjection/libinjection.i
+++ b/libinjection/libinjection.i
@@ -3,6 +3,8 @@
%{
#include "libinjection.h"
#include "libinjection_sqli.h"
+#include "libinjection_xss.h"
+#include "libinjection_error.h"
#include
/* This is the callback function that runs a python function
@@ -13,7 +15,6 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c
PyObject *fp;
PyObject *arglist;
PyObject *result;
- const char* strtype;
char ch;
// get sfilter->pattern
@@ -25,14 +26,21 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c
result = PyObject_CallObject((PyObject*) sf->userdata, arglist);
Py_DECREF(arglist);
if (result == NULL) {
- printf("GOT NULL\n");
// python call has an exception
// pass it back
ch = '\0';
} else {
- // convert value of python call to a char
- strtype = PyString_AsString(result);
- ch = strtype[0];
+ // convert value of python call to a char (Python 3 compatible)
+ if (PyUnicode_Check(result)) {
+ Py_ssize_t size;
+ const char* str = PyUnicode_AsUTF8AndSize(result, &size);
+ ch = (str != NULL && size > 0) ? str[0] : '\0';
+ } else if (PyBytes_Check(result)) {
+ const char* str = PyBytes_AsString(result);
+ ch = (str != NULL) ? str[0] : '\0';
+ } else {
+ ch = '\0';
+ }
Py_DECREF(result);
}
return ch;
@@ -67,6 +75,18 @@ for (i = 0; i < $1_dim0; i++) {
// automatically append string length into arg array
%apply (char *STRING, size_t LENGTH) { (const char *s, size_t slen) };
+%apply (char *STRING, size_t LENGTH) { (const char *s, size_t len) };
+
+// Make the fingerprint output parameter in libinjection_sqli() work as an output
+// The fingerprint buffer size matches libinjection's internal LIBINJECTION_SQLI_MAX_TOKENS (5) + null byte
+#define LIBINJECTION_FINGERPRINT_SIZE 8
+%typemap(in, numinputs=0) char fingerprint[] (char temp[LIBINJECTION_FINGERPRINT_SIZE]) {
+ memset(temp, 0, sizeof(temp));
+ $1 = temp;
+}
+%typemap(argout) char fingerprint[] {
+ $result = SWIG_Python_AppendOutput($result, PyUnicode_FromString($1));
+}
%typemap(in) (ptr_lookup_fn fn, void* userdata) {
if ($input == Py_None) {
@@ -77,5 +97,7 @@ for (i = 0; i < $1_dim0; i++) {
$2 = $input;
}
}
+%include "libinjection_error.h"
%include "libinjection.h"
%include "libinjection_sqli.h"
+%include "libinjection_xss.h"
diff --git a/libinjection/libinjection_error.h b/libinjection/libinjection_error.h
deleted file mode 100644
index 90c6713..0000000
--- a/libinjection/libinjection_error.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * LibInjection Project
- * BSD License -- see `COPYING.txt` for details
- *
- * https://github.com/libinjection/libinjection/
- *
- */
-
-#ifndef LIBINJECTION_ERROR_H
-#define LIBINJECTION_ERROR_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef enum injection_result_t {
- LIBINJECTION_RESULT_FALSE = 0,
- LIBINJECTION_RESULT_TRUE = 1,
- LIBINJECTION_RESULT_ERROR = -1
-} injection_result_t;
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif // LIBINJECTION_ERROR_H
diff --git a/setup.py b/setup.py
index eb47024..2fdcaad 100644
--- a/setup.py
+++ b/setup.py
@@ -5,20 +5,40 @@
nickg@client9.com
BSD License -- see COPYING.txt for details
"""
+import os
+
try:
from setuptools import setup, Extension
except ImportError:
from distutils.core import setup, Extension
+
+def get_libinjection_version():
+ """Read the libinjection version from the upstream source file, if available."""
+ version_file = os.path.join(os.path.dirname(__file__),
+ 'upstream', 'src', 'libinjection_sqli.c')
+ if os.path.exists(version_file):
+ with open(version_file) as f:
+ for line in f:
+ if '#define LIBINJECTION_VERSION' in line and '__clang_analyzer__' not in line:
+ # Extract version string from: #define LIBINJECTION_VERSION "x.y.z"
+ parts = line.strip().split('"')
+ if len(parts) >= 2:
+ return parts[1]
+ return 'undefined'
+
+
+LIBINJECTION_VERSION = get_libinjection_version()
+
MODULE = Extension(
- '_libinjection', [
+ 'libinjection._libinjection', [
'libinjection/libinjection_wrap.c',
'libinjection/libinjection_sqli.c',
'libinjection/libinjection_html5.c',
'libinjection/libinjection_xss.c'
],
swig_opts=['-Wextra', '-builtin'],
- define_macros = [],
+ define_macros = [('LIBINJECTION_VERSION', '"{}"'.format(LIBINJECTION_VERSION))],
include_dirs = [],
libraries = [],
library_dirs = [],
From beeae1a6842e896d855647dbd4a531b8aaf1ee0c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 22:57:18 +0000
Subject: [PATCH 4/4] Address review feedback: fix SWIG interface, Makefile,
test infrastructure, and add API tests
Co-authored-by: fzipi <3012076+fzipi@users.noreply.github.com>
---
Makefile | 28 +++++++++---
README.md | 12 ++++--
pytest.py => example_sqli.py | 0
json2python.py | 3 ++
libinjection/libinjection.i | 65 +++++++++++++++++++++++++---
setup.py | 2 +-
test_api.py | 84 ++++++++++++++++++++++++++++++++++++
test_driver.py | 47 ++++++++++++--------
8 files changed, 208 insertions(+), 33 deletions(-)
rename pytest.py => example_sqli.py (100%)
create mode 100644 test_api.py
diff --git a/Makefile b/Makefile
index 354c23a..ad93292 100644
--- a/Makefile
+++ b/Makefile
@@ -4,14 +4,13 @@ all: build
#
build: upstream libinjection/libinjection_wrap.c
- rm -f libinjection/libinjection.py
python3 setup.py --verbose build_ext --inplace
install: build
sudo python3 setup.py --verbose install
test-unit: build words.py
- cd /tmp && python3 -m pytest $(CURDIR)/test_driver.py -v
+ python3 -m pytest test_driver.py -v
.PHONY: test
test: test-unit
@@ -23,16 +22,35 @@ speed:
upstream:
[ -d $@ ] || git clone --depth=1 https://github.com/libinjection/libinjection.git upstream
-libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h: upstream
+libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h \
+libinjection/libinjection_xss.h libinjection/libinjection_html5.h: upstream
cp -f upstream/src/libinjection*.h upstream/src/libinjection*.c libinjection/
+ # Compatibility patches for SWIG wrapping: fix type mismatches and visibility.
+ # These sed invocations are pattern-matched to avoid breaking unrelated code.
+ #
+ # Fix return type mismatch: h5_state_data uses injection_result_t in definition but int in declaration
+ sed -i 's/^static int h5_state_data(/static injection_result_t h5_state_data(/' libinjection/libinjection_html5.c
+ # Fix return type mismatch: libinjection_is_sqli declared as injection_result_t but defined as int
+ sed -i 's/^int libinjection_is_sqli(/injection_result_t libinjection_is_sqli(/' libinjection/libinjection_sqli.c
+ # Remove static from helper functions so SWIG can wrap and expose them to Python
+ # (static functions in a header cannot be called from libinjection_wrap.c)
+ sed -i 's/^static void libinjection_sqli_reset(/void libinjection_sqli_reset(/' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c
+ sed -i ':a;N;$$!ba;s/static char\nlibinjection_sqli_lookup_word/char\nlibinjection_sqli_lookup_word/g' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c
+ sed -i ':a;N;$$!ba;s/static int\nlibinjection_sqli_blacklist/int\nlibinjection_sqli_blacklist/g' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c
+ sed -i ':a;N;$$!ba;s/static int\nlibinjection_sqli_not_whitelist/int\nlibinjection_sqli_not_whitelist/g' libinjection/libinjection_sqli.h libinjection/libinjection_sqli.c
words.py: Makefile json2python.py upstream
python3 json2python.py < upstream/src/sqlparse_data.json > words.py
-libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/libinjection_error.h
+libinjection/libinjection_wrap.c: libinjection/libinjection.i libinjection/libinjection.h \
+libinjection/libinjection_sqli.h libinjection/libinjection_error.h \
+libinjection/libinjection_xss.h libinjection/libinjection_html5.h
swig -version
- swig -python -builtin -Wall -Wextra libinjection/libinjection.i
+ swig -python -builtin -Wall -Wextra \
+ -o libinjection/libinjection_wrap.c \
+ -outdir libinjection \
+ libinjection/libinjection.i
.PHONY: copy
diff --git a/README.md b/README.md
index 3d85508..e22005a 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,10 @@ make libinjection/libinjection.h libinjection/libinjection_sqli.h libinjection/l
### 3. Generate the SWIG wrapper
```bash
-swig -python -builtin -Wall -Wextra libinjection/libinjection.i
+swig -python -builtin -Wall -Wextra \
+ -o libinjection/libinjection_wrap.c \
+ -outdir libinjection \
+ libinjection/libinjection.i
```
### 4. Build the Python extension
@@ -85,9 +88,10 @@ if result:
## Testing
-Run the test suite using pytest:
+Run the test suite using pytest from the repository root:
```bash
-cd /tmp # run from outside the repo directory to avoid pytest.py conflict
-python3 -m pytest /path/to/python3-libinjection/test_driver.py -v
+python3 -m pytest test_driver.py test_api.py -v
```
+
+> **Note:** `upstream/tests/` must exist (run `make upstream` first) for `test_driver.py` to find test data.
diff --git a/pytest.py b/example_sqli.py
similarity index 100%
rename from pytest.py
rename to example_sqli.py
diff --git a/json2python.py b/json2python.py
index 83fedc6..381fc5e 100755
--- a/json2python.py
+++ b/json2python.py
@@ -16,6 +16,9 @@ def toc(obj):
import libinjection
def lookup(state, stype, keyword):
+ # keyword is passed as bytes from C; decode to str for dict lookup
+ if isinstance(keyword, bytes):
+ keyword = keyword.decode('latin-1')
keyword = keyword.upper()
if stype == libinjection.LOOKUP_FINGERPRINT:
if keyword in fingerprints and libinjection.sqli_not_whitelist(state):
diff --git a/libinjection/libinjection.i b/libinjection/libinjection.i
index 8d91dff..2a9b370 100644
--- a/libinjection/libinjection.i
+++ b/libinjection/libinjection.i
@@ -6,6 +6,7 @@
#include "libinjection_xss.h"
#include "libinjection_error.h"
#include
+#include
/* This is the callback function that runs a python function
*
@@ -21,7 +22,14 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c
// convert to python string
fp = SWIG_InternalNewPointerObj((void*)sf, SWIGTYPE_p_libinjection_sqli_state,0);
- arglist = Py_BuildValue("(Nis#)", fp, lookuptype, word, len);
+ // Use y# (bytes) format instead of s# (str) to avoid UnicodeDecodeError on
+ // non-UTF-8 bytes (e.g. 0xA0 word separators). The Python callback will
+ // receive the word as a bytes object and should decode it as needed.
+ arglist = Py_BuildValue("(Niy#)", fp, lookuptype, word, len);
+ if (arglist == NULL) {
+ // Py_BuildValue failed (e.g., encoding error); treat as not found
+ return '\0';
+ }
// call pyfunct with string arg
result = PyObject_CallObject((PyObject*) sf->userdata, arglist);
Py_DECREF(arglist);
@@ -34,7 +42,13 @@ static char libinjection_python_check_fingerprint(sfilter* sf, int lookuptype, c
if (PyUnicode_Check(result)) {
Py_ssize_t size;
const char* str = PyUnicode_AsUTF8AndSize(result, &size);
- ch = (str != NULL && size > 0) ? str[0] : '\0';
+ if (str != NULL && size > 0) {
+ ch = str[0];
+ } else {
+ // Clear any exception set by PyUnicode_AsUTF8AndSize on failure
+ PyErr_Clear();
+ ch = '\0';
+ }
} else if (PyBytes_Check(result)) {
const char* str = PyBytes_AsString(result);
ch = (str != NULL) ? str[0] : '\0';
@@ -73,9 +87,50 @@ for (i = 0; i < $1_dim0; i++) {
}
}
-// automatically append string length into arg array
-%apply (char *STRING, size_t LENGTH) { (const char *s, size_t slen) };
-%apply (char *STRING, size_t LENGTH) { (const char *s, size_t len) };
+// automatically append string length into arg array.
+// Accept both str (encoded as UTF-8) and bytes (passed through as-is).
+// Using bytes is recommended when the input may contain non-ASCII octets,
+// since str will be UTF-8 encoded which changes the byte values.
+%typemap(in) (const char *s, size_t slen) (Py_buffer _view, int _must_release) {
+ _must_release = 0;
+ if (PyBytes_Check($input)) {
+ if (PyObject_GetBuffer($input, &_view, PyBUF_SIMPLE) != 0) SWIG_fail;
+ $1 = (const char *)_view.buf;
+ $2 = (size_t)_view.len;
+ _must_release = 1;
+ } else if (PyUnicode_Check($input)) {
+ Py_ssize_t _len;
+ $1 = PyUnicode_AsUTF8AndSize($input, &_len);
+ if (!$1) SWIG_fail;
+ $2 = (size_t)_len;
+ } else {
+ PyErr_SetString(PyExc_TypeError, "expected str or bytes");
+ SWIG_fail;
+ }
+}
+%typemap(freearg) (const char *s, size_t slen) {
+ if (_must_release$argnum) PyBuffer_Release(&_view$argnum);
+}
+%typemap(in) (const char *s, size_t len) (Py_buffer _view, int _must_release) {
+ _must_release = 0;
+ if (PyBytes_Check($input)) {
+ if (PyObject_GetBuffer($input, &_view, PyBUF_SIMPLE) != 0) SWIG_fail;
+ $1 = (const char *)_view.buf;
+ $2 = (size_t)_view.len;
+ _must_release = 1;
+ } else if (PyUnicode_Check($input)) {
+ Py_ssize_t _len;
+ $1 = PyUnicode_AsUTF8AndSize($input, &_len);
+ if (!$1) SWIG_fail;
+ $2 = (size_t)_len;
+ } else {
+ PyErr_SetString(PyExc_TypeError, "expected str or bytes");
+ SWIG_fail;
+ }
+}
+%typemap(freearg) (const char *s, size_t len) {
+ if (_must_release$argnum) PyBuffer_Release(&_view$argnum);
+}
// Make the fingerprint output parameter in libinjection_sqli() work as an output
// The fingerprint buffer size matches libinjection's internal LIBINJECTION_SQLI_MAX_TOKENS (5) + null byte
diff --git a/setup.py b/setup.py
index 2fdcaad..b4010e1 100644
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@ def get_libinjection_version():
version_file = os.path.join(os.path.dirname(__file__),
'upstream', 'src', 'libinjection_sqli.c')
if os.path.exists(version_file):
- with open(version_file) as f:
+ with open(version_file, encoding="utf-8") as f:
for line in f:
if '#define LIBINJECTION_VERSION' in line and '__clang_analyzer__' not in line:
# Extract version string from: #define LIBINJECTION_VERSION "x.y.z"
diff --git a/test_api.py b/test_api.py
new file mode 100644
index 0000000..afaa376
--- /dev/null
+++ b/test_api.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+"""
+API tests for libinjection Python bindings.
+Covers the simple sqli() and xss() APIs as well as the stateful sqli API.
+"""
+import libinjection
+
+
+def test_sqli_returns_tuple():
+ """sqli() should return a (result, fingerprint) sequence."""
+ result = libinjection.sqli("1 UNION SELECT * FROM users")
+ assert len(result) == 2, "sqli() must return a 2-element sequence (result, fingerprint)"
+
+
+def test_sqli_detects_injection():
+ """sqli() must detect a known SQLi payload."""
+ is_sqli, fingerprint = libinjection.sqli("1 UNION SELECT * FROM users")
+ assert is_sqli == 1, "Expected SQLi to be detected"
+ assert fingerprint != "", "Expected non-empty fingerprint for SQLi input"
+
+
+def test_sqli_benign_input():
+ """sqli() must not flag benign input."""
+ is_sqli, fingerprint = libinjection.sqli("hello world")
+ assert is_sqli == 0, "Benign input should not be flagged as SQLi"
+ assert fingerprint == "", "Benign input should produce an empty fingerprint"
+
+
+def test_sqli_fingerprint_content():
+ """sqli() fingerprint should be a non-empty string for detected SQLi."""
+ is_sqli, fingerprint = libinjection.sqli("1 UNION ALL SELECT * FROM foo")
+ assert is_sqli == 1
+ assert isinstance(fingerprint, str)
+ assert len(fingerprint) > 0
+
+
+def test_is_sqli_stateful_api():
+ """Advanced stateful API using sqli_state / sqli_init / sqli_callback / is_sqli."""
+ state = libinjection.sqli_state()
+ libinjection.sqli_init(
+ state,
+ "1 UNION SELECT * FROM users",
+ libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI,
+ )
+ libinjection.sqli_callback(state, None)
+ assert libinjection.is_sqli(state) == 1, "Expected SQLi detection via stateful API"
+ assert state.fingerprint != "", "Expected fingerprint set in state"
+
+
+def test_is_sqli_stateful_benign():
+ """Stateful API should not flag benign input."""
+ state = libinjection.sqli_state()
+ libinjection.sqli_init(
+ state,
+ "hello world",
+ libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI,
+ )
+ libinjection.sqli_callback(state, None)
+ assert libinjection.is_sqli(state) == 0, "Benign input should not be SQLi"
+
+
+def test_xss_detects_script_tag():
+ """xss() must detect a basic XSS payload."""
+ result = libinjection.xss("")
+ assert result == 1, "Expected XSS detection for