firegex/tests/unit/test_regex_library.py
2026-09-13 15:54:56 +02:00

95 lines
3.7 KiB
Python

"""`firegex.regex`, and the promise that trying a ruleset there means something.
The library exists so a ruleset can be tried before it reaches a running instance. That
is only worth anything if the answers match, so these are mostly about agreement with the
datapath: the same engine, the same validity, the same precedence. Nothing here needs a
firegex instance — only libhs, which is the same library the product loads, bound through
ctypes rather than reimplemented.
"""
import pytest
regex = pytest.importorskip("firegex.regex")
pytestmark = pytest.mark.skipif(
not regex.available(),
reason="libhs is not installed here, so there is no engine to agree with",
)
def test_a_good_pattern_validates():
assert regex.validate(regex.Rule(id="a", pattern="FLAG")) is None
def test_a_broken_pattern_reports_the_engines_own_words():
why = regex.validate(regex.Rule(id="b", pattern="(unclosed"))
assert why is not None
assert "parenthesis" in why.lower(), why
def test_a_bounded_repeat_is_judged_by_the_mode_it_will_run_in():
"""`a{1,1000}b` streams but does not block-scan.
A pattern is compiled for stream matching, because that is how it follows a
connection across chunk boundaries. Judging it by the other mode would refuse a rule
that works — the same class of lie as a tester that disagrees with the engine.
"""
assert regex.validate(regex.Rule(id="c", pattern="a{1,1000}b")) is None
def test_a_matching_chunk_is_refused_naming_the_rule():
rules = regex.Ruleset([regex.Rule(id="block-me", pattern="BLOCKME")])
assert rules.apply(b"carrying BLOCKME", True) == "block-me"
def test_a_clean_chunk_passes():
rules = regex.Ruleset([regex.Rule(id="block-me", pattern="BLOCKME")])
assert rules.apply(b"nothing here", True) is None
def test_direction_is_honoured_both_ways():
directed = regex.Ruleset(
[regex.Rule(id="out", pattern="FLAG", direction=regex.Direction.S2C)])
assert directed.apply(b"where is the FLAG", True) is None
assert directed.apply(b"here: FLAG", False) == "out"
def test_case_insensitivity_is_per_rule():
insensitive = regex.Ruleset(
[regex.Rule(id="any", pattern="NeEdLe", case_sensitive=False)])
assert insensitive.apply(b"needle", True) == "any"
def test_patterns_match_bytes_that_are_not_text():
raw = regex.Ruleset([regex.Rule(id="bin", pattern=r"\x00\xff\xfe")])
assert raw.apply(bytes([0x41, 0x00, 0xFF, 0xFE, 0x42]), True) == "bin"
def test_a_firegex_shaped_ruleset_loads_and_keeps_what_it_said():
loaded = regex.load_rules([
{"id": "r1", "pattern": "x", "direction": "c2s"},
{"id": "r2", "pattern": "y", "case_sensitive": False},
])
assert len(loaded) == 2
assert loaded[0].direction is regex.Direction.C2S
assert loaded[1].case_sensitive is False
def test_a_ruleset_asking_to_rewrite_is_refused_rather_than_quietly_blocking():
"""Rewriting was withdrawn from firegex, so it has to be withdrawn here too.
A simulator that accepted a rule the product cannot express — and then blocked on it
instead — would be exactly the disagreement this library exists to make impossible.
Silently treating `rewrite` as `block` is the worse failure of the two, because the
ruleset would look like it had been understood.
"""
with pytest.raises(ValueError, match="can only block"):
regex.load_rules([
{"id": "r", "pattern": "FLAG", "action": "rewrite", "with": "redacted"},
])
def test_an_explicit_block_action_is_still_accepted():
"""Old ruleset files carry it, and it says exactly what happens."""
assert regex.load_rules([{"id": "r", "pattern": "x", "action": "block"}])[0].id == "r"