You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by cm...@apache.org on 2026/09/14 22:27:39 UTC
(trafficserver) branch master updated: hrw4u: Fix u4wrh emitting HTTP-section operators into TXN_CLOSE (#13204)
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 37c5eadb80 hrw4u: Fix u4wrh emitting HTTP-section operators into TXN_CLOSE (#13204)
37c5eadb80 is described below
commit 37c5eadb80b0880402f7af7b46e0c61893132cc6
Author: Robert Clendenin <rc...@apple.com>
AuthorDate: Mon Sep 14 15:27:33 2026 -0700
hrw4u: Fix u4wrh emitting HTTP-section operators into TXN_CLOSE (#13204)
u4wrh bypassed section access validation when emitting statements, so
operators that declare sections=HTTP_SECTIONS could land inside a generated
TXN_CLOSE block. The output then failed re-parse in hrw4u. Validate the
operator's declared sections at emit time and surface a clear error when
the originating HRW places it in an incompatible hook.
Adds a dedicated OPERATOR_MAP reverse-fail fixture for the operator-level
path and documents the *.reverse.fail.* fixture naming convention.
---
tools/hrw4u/src/hrw_symbols.py | 8 +++--
tools/hrw4u/src/hrw_visitor.py | 7 ++--
.../hooks/txn_close_http_op.reverse.fail.error.txt | 1 +
.../hooks/txn_close_http_op.reverse.fail.hrw.txt | 3 ++
...xn_close_set_destination.reverse.fail.error.txt | 1 +
.../txn_close_set_destination.reverse.fail.hrw.txt | 2 ++
tools/hrw4u/tests/test_hooks_reverse.py | 9 +++++
tools/hrw4u/tests/test_ops_reverse.py | 9 +++++
tools/hrw4u/tests/utils.py | 42 ++++++++++++++++++++++
9 files changed, 78 insertions(+), 4 deletions(-)
diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py
index 2d6721bbe3..c12fb765f4 100644
--- a/tools/hrw4u/src/hrw_symbols.py
+++ b/tools/hrw4u/src/hrw_symbols.py
@@ -190,8 +190,7 @@ class InverseSymbolResolver(SymbolResolverBase):
if tag == "HEADER":
return f"{self.get_prefix_for_context('header_condition', section)}{suffix}", False
- else:
- return f"{lhs_prefix}{suffix.replace(':', '.')}", False
+ return f"{lhs_prefix}{suffix.replace(':', '.')}", False
return None
def _should_lowercase_suffix(self, tag_match: str, lhs_prefix: str) -> bool:
@@ -522,10 +521,15 @@ class InverseSymbolResolver(SymbolResolverBase):
commands = params.target if params else None
if (isinstance(commands, (list, tuple)) and cmd in commands) or (cmd == commands):
uppercase = params.upper if params else False
+ if params:
+ qualifier = toks[1] if len(toks) > 1 else ""
+ name = f"{lhs_key}{qualifier}" if lhs_key.endswith(".") else lhs_key
+ self.validate_section_access(name, section, params.sections)
return self._handle_operator_command(cmd, toks, lhs_key, uppercase, section)
for name, params in tables.STATEMENT_FUNCTION_MAP.items():
if params.target == cmd:
+ self.validate_section_access(name, section, params.sections)
return self._handle_statement_function(name, args, section, op_state)
raise SymbolResolutionError(line, f"Unknown operator: {cmd}")
diff --git a/tools/hrw4u/src/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py
index b6dc61a90d..7d33aff920 100644
--- a/tools/hrw4u/src/hrw_visitor.py
+++ b/tools/hrw4u/src/hrw_visitor.py
@@ -371,8 +371,11 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
args = self._reconstruct_redirect_args(args)
self.debug(f"reconstructed redirect: {args}")
- stmt = self.symbol_resolver.op_to_hrw4u(cmd, args, self._section_label, op_state)
- self.emit(stmt + ";")
+ stmt = None
+ with self.trap(ctx):
+ stmt = self.symbol_resolver.op_to_hrw4u(cmd, args, self._section_label, op_state)
+ if stmt is not None:
+ self.emit(stmt + ";")
return None
diff --git a/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.error.txt b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.error.txt
new file mode 100644
index 0000000000..3369fe1893
--- /dev/null
+++ b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.error.txt
@@ -0,0 +1 @@
+is not available in the TXN_CLOSE section
diff --git a/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.hrw.txt b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.hrw.txt
new file mode 100644
index 0000000000..e64852983a
--- /dev/null
+++ b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.hrw.txt
@@ -0,0 +1,3 @@
+cond %{TXN_CLOSE_HOOK} [AND]
+cond %{CLIENT-HEADER:@sampleratio} ="0.05"
+ add-header @TCPInfo "TC; %{TCP-INFO}"
diff --git a/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.error.txt b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.error.txt
new file mode 100644
index 0000000000..3369fe1893
--- /dev/null
+++ b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.error.txt
@@ -0,0 +1 @@
+is not available in the TXN_CLOSE section
diff --git a/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.hrw.txt b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.hrw.txt
new file mode 100644
index 0000000000..5e366e4baa
--- /dev/null
+++ b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.hrw.txt
@@ -0,0 +1,2 @@
+cond %{TXN_CLOSE_HOOK} [AND]
+ set-destination HOST "foo"
diff --git a/tools/hrw4u/tests/test_hooks_reverse.py b/tools/hrw4u/tests/test_hooks_reverse.py
index 5db1958aff..1111ff43f1 100644
--- a/tools/hrw4u/tests/test_hooks_reverse.py
+++ b/tools/hrw4u/tests/test_hooks_reverse.py
@@ -28,3 +28,12 @@ import utils
def test_reverse_conversion(input_file: Path, output_file: Path) -> None:
"""Test that u4wrh reverse conversion produces original hrw4u for hooks test cases."""
utils.run_reverse_test(input_file, output_file)
+
+
+@pytest.mark.hooks
+@pytest.mark.reverse
+@pytest.mark.invalid
+@pytest.mark.parametrize("input_file", utils.collect_reverse_failing_inputs("hooks"))
+def test_reverse_invalid_inputs_fail(input_file: Path) -> None:
+ """Test that u4wrh surfaces section access errors for invalid HRW hook inputs."""
+ utils.run_reverse_failing_test(input_file)
diff --git a/tools/hrw4u/tests/test_ops_reverse.py b/tools/hrw4u/tests/test_ops_reverse.py
index 9194c165e2..2832359a7f 100644
--- a/tools/hrw4u/tests/test_ops_reverse.py
+++ b/tools/hrw4u/tests/test_ops_reverse.py
@@ -28,3 +28,12 @@ import utils
def test_reverse_conversion(input_file: Path, output_file: Path) -> None:
"""Test that u4wrh reverse conversion produces original hrw4u for ops test cases."""
utils.run_reverse_test(input_file, output_file)
+
+
+@pytest.mark.ops
+@pytest.mark.reverse
+@pytest.mark.invalid
+@pytest.mark.parametrize("input_file", utils.collect_reverse_failing_inputs("ops"))
+def test_reverse_invalid_inputs_fail(input_file: Path) -> None:
+ """Test that u4wrh surfaces section access errors for invalid HRW operator inputs."""
+ utils.run_reverse_failing_test(input_file)
diff --git a/tools/hrw4u/tests/utils.py b/tools/hrw4u/tests/utils.py
index bd893e7291..941fe1d615 100644
--- a/tools/hrw4u/tests/utils.py
+++ b/tools/hrw4u/tests/utils.py
@@ -40,12 +40,14 @@ __all__: Final[list[str]] = [
"collect_output_test_files",
"collect_ast_test_files",
"collect_failing_inputs",
+ "collect_reverse_failing_inputs",
"collect_sandbox_deny_test_files",
"collect_sandbox_allow_test_files",
"collect_sandbox_warn_test_files",
"run_output_test",
"run_ast_test",
"run_failing_test",
+ "run_reverse_failing_test",
"run_sandbox_deny_test",
"run_sandbox_allow_test",
"run_sandbox_warn_test",
@@ -128,6 +130,17 @@ def collect_failing_inputs(group: str) -> Iterator[pytest.param]:
yield pytest.param(input_file, id=test_id)
+def collect_reverse_failing_inputs(group: str) -> Iterator[pytest.param]:
+ # Reverse-fail fixtures use *.reverse.fail.hrw.txt (u4wrh input that must error)
+ # paired with *.reverse.fail.error.txt (expected error phrase). Forward-fail
+ # fixtures use *.fail.input.txt for the hrw4u compiler, which is why the glob
+ # here excludes them.
+ base_dir = Path("tests/data") / group
+ for input_file in base_dir.glob("*.reverse.fail.hrw.txt"):
+ test_id = input_file.stem
+ yield pytest.param(input_file, id=test_id)
+
+
def _collect_sandbox_test_files(group: str, result_suffix: str) -> Iterator[pytest.param]:
"""Collect sandbox test files: (input, result, sandbox_config).
@@ -373,6 +386,35 @@ def run_reverse_test(input_file: Path, output_file: Path, debug: bool = False) -
assert actual_hrw4u == expected_hrw4u, f"Reverse conversion mismatch for {output_file}"
+def run_reverse_failing_test(input_file: Path) -> None:
+ hrw_text = input_file.read_text()
+ lexer = u4wrhLexer(InputStream(hrw_text))
+ stream = CommonTokenStream(lexer)
+ parser = u4wrhParser(stream)
+ tree = parser.program()
+
+ error_file = input_file.with_name(input_file.name.replace(".reverse.fail.hrw.txt", ".reverse.fail.error.txt"))
+ if not error_file.exists():
+ raise RuntimeError(f"Missing expected error file: {error_file}")
+
+ expected_error_content = error_file.read_text().strip()
+
+ error_collector = ErrorCollector()
+ visitor = HRWInverseVisitor(filename=str(input_file), error_collector=error_collector)
+ visitor.visit(tree)
+
+ assert error_collector.has_errors(), f"Expected reverse errors but none were raised for {input_file}"
+
+ actual_summary = error_collector.get_error_summary()
+ for line in expected_error_content.splitlines():
+ line = line.strip()
+ if line:
+ assert line in actual_summary, (
+ f"Expected phrase not found in error summary for {input_file}:\n"
+ f" Missing: {line!r}\n"
+ f"Actual summary:\n{actual_summary}")
+
+
def create_output_test(group: str):
import pytest