Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,13 @@ any Python object passed to a ``scan`` method.

The return value determines whether or not Hyperscan should halt
scanning. If the match handler returns anything other than ``None``
that is *truthy*, scanning will be halted and any subsequent calls to
``Database.scan`` or ``Stream.scan`` will throw a ``hyperscan.error``.
that is *truthy*, the scan raises ``hyperscan.ScanTerminated``. A terminated
stream also raises this exception on subsequent ``Stream.scan`` calls;
block and vectored databases can be scanned again.

Exceptions raised by a callback, including errors when evaluating its
return value's truthiness, propagate from ``Database.scan`` or ``Stream.scan``
without being replaced by ``ScanTerminated``.

## Pattern Scanning

Expand Down
12 changes: 10 additions & 2 deletions src/hyperscan/extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,9 @@ static int hs_match_handler(
cctx->success = 0;
} else {
halt = rv == Py_None ? 0 : PyObject_IsTrue(rv);
cctx->success = 1;
cctx->success = halt >= 0;
if (halt < 0)
halt = 1;
}
Py_XDECREF(rv);
PyGILState_Release(gstate);
Expand Down Expand Up @@ -285,7 +287,9 @@ static int ch_match_handler(
cctx->success = 0;
} else {
halt = rv == Py_None ? 0 : PyObject_IsTrue(rv);
cctx->success = 1;
cctx->success = halt >= 0;
if (halt < 0)
halt = 1;
}
Py_XDECREF(rv);
Py_XDECREF(ocaptured);
Expand Down Expand Up @@ -772,6 +776,8 @@ static PyObject *Database_scan(Database *self, PyObject *args, PyObject *kwds)
PyMem_RawFree(data);
PyMem_RawFree(lengths);
Py_XDECREF(fast_seq);
if (PyErr_Occurred())
HS_LOCK_RETURN_NULL();
HANDLE_HYPERSCAN_ERR(hs_err, NULL);
} else {
if (!PyObject_CheckBuffer(odata)) {
Expand Down Expand Up @@ -1182,6 +1188,8 @@ static PyObject *Stream_scan(Stream *self, PyObject *args, PyObject *kwds)
ocallback == Py_None ? NULL : (void *)&cctx);
Py_END_ALLOW_THREADS;
PyBuffer_Release(&view);
if (PyErr_Occurred())
HS_LOCK_RETURN_NULL();
HANDLE_HYPERSCAN_ERR(hs_err, NULL);
}

Expand Down
38 changes: 34 additions & 4 deletions tests/test_hyperscan.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,41 @@ def test_database_deserialize_scan(db_fixture_name, request, mocker):
db.scan(buf, match_event_handler=callback)


def test_database_exception_in_callback(database_block, mocker):
callback = mocker.Mock(side_effect=RuntimeError("oops"))
@pytest.mark.parametrize(
"db_fixture_name",
["database_block", "database_vector", "database_stream", "database_chimera"],
)
@pytest.mark.parametrize("failure", ["raise", "bool", "halt"])
def test_database_exception_in_callback(db_fixture_name, failure, request, mocker):
database = request.getfixturevalue(db_fixture_name)
error = RuntimeError("oops")

with pytest.raises(RuntimeError, match=r"^oops$"):
database_block.scan(b"foobar", match_event_handler=callback)
class InvalidTruth:
def __bool__(self):
raise error

if failure == "raise":
callback = mocker.Mock(side_effect=error)
else:
callback = mocker.Mock(
return_value=True if failure == "halt" else InvalidTruth()
)
expected = hyperscan.ScanTerminated if failure == "halt" else RuntimeError

with pytest.raises(expected) as caught:
if db_fixture_name == "database_stream":
with database.stream(match_event_handler=callback) as stream:
stream.scan(b"foobar")
else:
data = (
[b"foo", b"bar"] if db_fixture_name == "database_vector" else b"foobar"
)
database.scan(data, match_event_handler=callback)
if failure != "halt":
assert caught.value is error
callback.assert_called_once()
if db_fixture_name != "database_stream":
database.scan(data, match_event_handler=lambda *args: None)


def test_literal_expressions(mocker):
Expand Down