diff --git a/docs/usage.md b/docs/usage.md index a4ec38d..07c4fb9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -35,6 +35,11 @@ The only required parameter to ``hyperscan.Database`` is rest of the parameters, including **ids**, **elements**, and **flags** are optional. +Expression IDs and flag values must be integers in the unsigned 32-bit range +(`0` through `2**32 - 1`). Values outside this range raise `OverflowError` +instead of being truncated. Flags can be supplied as a single integer or a +sequence of integers; the native engine also checks supported flag combinations. + ```python import hyperscan diff --git a/src/hyperscan/extension.c b/src/hyperscan/extension.c index e072d10..5fa8e98 100644 --- a/src/hyperscan/extension.c +++ b/src/hyperscan/extension.c @@ -344,6 +344,20 @@ static int Database_init(Database *self, PyObject *args, PyObject *kwds) return 0; } +static int parse_uint32(PyObject *value, uint32_t *result) +{ + unsigned long number = PyLong_AsUnsignedLong(value); + if (PyErr_Occurred()) + return -1; + if (number > UINT32_MAX) { + PyErr_SetString( + PyExc_OverflowError, "value exceeds an unsigned 32-bit integer"); + return -1; + } + *result = (uint32_t)number; + return 0; +} + static PyObject *Database_compile( Database *self, PyObject *args, PyObject *kwds) { @@ -416,9 +430,11 @@ static PyObject *Database_compile( if (ids == NULL) goto memory_error; - globalflag = (oflags == Py_None ? 0 : PyLong_AsUnsignedLong(oflags)); - - PyErr_Clear(); + globalflag = 0; + if ( + oflags != Py_None && !PySequence_Check(oflags) && + parse_uint32(oflags, &globalflag) < 0) + goto python_error; for (uint64_t i = 0; i < elements; i++) { const char *expression; @@ -450,8 +466,7 @@ static PyObject *Database_compile( if (PyObject_IsTrue(oids)) { oid = PySequence_ITEM(oids, i); - expr_id = PyLong_AsUnsignedLong(oid); - if (PyErr_Occurred()) + if (parse_uint32(oid, &expr_id) < 0) break; } else { expr_id = i; @@ -461,8 +476,7 @@ static PyObject *Database_compile( oflag = PySequence_ITEM(oflags, i); if (PyErr_Occurred()) break; - expr_flags = PyLong_AsUnsignedLong(oflag); - if (PyErr_Occurred()) + if (parse_uint32(oflag, &expr_flags) < 0) break; } else { expr_flags = globalflag; diff --git a/tests/test_compile_parameters.py b/tests/test_compile_parameters.py new file mode 100644 index 0000000..d09d846 --- /dev/null +++ b/tests/test_compile_parameters.py @@ -0,0 +1,55 @@ +import pytest + +import hyperscan + + +@pytest.fixture(params=["regex", "literal", "chimera"]) +def compiler(request): + chimera = request.param == "chimera" + db = hyperscan.Database( + chimera=chimera, + mode=hyperscan.CH_MODE_GROUPS if chimera else hyperscan.HS_MODE_BLOCK, + ) + + def compile(**kwargs): + db.compile(expressions=[b"foo"], literal=request.param == "literal", **kwargs) + + return ( + db, + compile, + hyperscan.CH_FLAG_CASELESS if chimera else hyperscan.HS_FLAG_CASELESS, + ) + + +@pytest.mark.parametrize( + "flags, error", + [ + (0.0, TypeError), + (-1, OverflowError), + (1 << 32, OverflowError), + ([1 << 32], OverflowError), + ([0.0], TypeError), + (object(), TypeError), + ], +) +def test_invalid_compile_flags(compiler, flags, error): + _, compile, _ = compiler + with pytest.raises(error): + compile(flags=flags) + + +@pytest.mark.parametrize("identifier", [-1, 1 << 32, (1 << 32) + 7]) +def test_invalid_compile_ids(compiler, identifier): + _, compile, _ = compiler + with pytest.raises(OverflowError): + compile(ids=[identifier]) + + +@pytest.mark.parametrize("identifier", [0, (1 << 32) - 1]) +@pytest.mark.parametrize("sequence_flags", [False, True]) +def test_valid_compile_parameter_boundaries(compiler, identifier, sequence_flags): + db, compile, flag = compiler + compile(ids=[identifier], flags=[flag] if sequence_flags else flag) + matches = [] + db.scan(b"FOO", match_event_handler=lambda *args: matches.append(args[0])) + assert matches == [identifier]