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