This is an automated email from the ASF dual-hosted git repository.
zwoop 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 e319f593b5 Hrw: Supports nested If (#12562)
e319f593b5 is described below
commit e319f593b51d55f983c970822ad45aa04975a7ea
Author: Leif Hedstrom <[email protected]>
AuthorDate: Mon Oct 27 09:25:19 2025 -0700
Hrw: Supports nested If (#12562)
* HRW: Adds support for nested If operators
* HRW docs additions and cleanup for new operator
* Adds hrw4u / u4wrh support for nested If's
* HRW4U: Allow for explicit state variable slot decl
---
doc/admin-guide/configuration/hrw4u.en.rst | 26 +++-
doc/admin-guide/plugins/header_rewrite.en.rst | 144 ++++++++++++++++++--
plugins/header_rewrite/factory.cc | 1 +
plugins/header_rewrite/header_rewrite.cc | 103 +++++++++++---
plugins/header_rewrite/operator.h | 15 ++
plugins/header_rewrite/operators.cc | 123 +++++++++++++++++
plugins/header_rewrite/operators.h | 80 +++++++++++
plugins/header_rewrite/parser.cc | 7 +
plugins/header_rewrite/parser.h | 14 +-
plugins/header_rewrite/ruleset.cc | 54 ++++++--
plugins/header_rewrite/ruleset.h | 151 ++++++---------------
.../header_rewrite/gold/nested_ifs_definitely.gold | 18 +++
.../header_rewrite/gold/nested_ifs_else.gold | 17 +++
.../header_rewrite/gold/nested_ifs_else_fie.gold | 19 +++
.../header_rewrite/gold/nested_ifs_foo_bar.gold | 20 +++
.../header_rewrite/gold/nested_ifs_foo_fie.gold | 21 +++
.../header_rewrite/gold/nested_ifs_maybe.gold | 18 +++
.../header_rewrite/header_rewrite_bundle.test.py | 39 ++++++
.../header_rewrite/rules/implicit_hook.conf | 2 -
.../rules/{implicit_hook.conf => nested_ifs.conf} | 46 ++++---
tools/hrw4u/grammar/hrw4u.g4 | 4 +-
tools/hrw4u/grammar/u4wrh.g4 | 48 ++++---
tools/hrw4u/pyproject.toml | 2 +-
tools/hrw4u/scripts/testcase.py | 71 +++++++++-
tools/hrw4u/src/hrw_visitor.py | 55 +++++---
tools/hrw4u/src/symbols.py | 25 +++-
tools/hrw4u/src/types.py | 6 +-
tools/hrw4u/src/visitor.py | 24 +++-
tools/hrw4u/tests/data/conds/nested-ifs.ast.txt | 1 +
tools/hrw4u/tests/data/conds/nested-ifs.input.txt | 27 ++++
tools/hrw4u/tests/data/conds/nested-ifs.output.txt | 27 ++++
.../data/ops/http_cntl_invalid_bool.fail.error.txt | 4 +-
.../data/ops/http_cntl_quoted_bool.fail.error.txt | 4 +-
.../data/ops/skip_remap_quoted_bool.fail.error.txt | 4 +-
tools/hrw4u/tests/data/vars/exceptions.txt | 5 +
tools/hrw4u/tests/data/vars/explicit_slots.ast.txt | 1 +
.../hrw4u/tests/data/vars/explicit_slots.input.txt | 15 ++
.../tests/data/vars/explicit_slots.output.txt | 3 +
.../tests/data/vars/slot_conflict.fail.error.txt | 3 +
.../tests/data/vars/slot_conflict.fail.input.txt | 8 ++
.../tests/data/vars/vars_count.fail.error.txt | 2 +-
41 files changed, 1013 insertions(+), 244 deletions(-)
diff --git a/doc/admin-guide/configuration/hrw4u.en.rst
b/doc/admin-guide/configuration/hrw4u.en.rst
index 39002b919d..11bb275daf 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -323,9 +323,29 @@ TXN_CLOSE_HOOK TXN_CLOSE
End of transaction
A special section `VARS` is used to declare variables. There is no equivalent
in
`header_rewrite`, where you managed the variables manually.
-.. note::
- The section name is always required in HRW4U, there are no implicit or
default hooks. There
- can be several if/else block per section block.
+Variables and State Slots
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Each variable type has a limited number of slots available:
+
+- ``bool`` - 16 slots (0-15)
+- ``int8`` - 4 slots (0-3)
+- ``int16`` - 1 slot (0)
+
+By default, slots are assigned automatically in declaration order. You can
explicitly assign
+a slot number using the ``@`` syntax::
+
+ VARS {
+ priority: bool @7; # Explicitly use slot 7
+ active: bool; # Auto-assigned to slot 0
+ config: bool @12; # Explicitly use slot 12
+ counter: int8 @2; # Explicitly use int8 slot 2
+ }
+
+Explicit slot assignment is useful when you need predictable slot numbers
across configurations
+or when integrating with existing header_rewrite rules that reference specific
slot numbers. In
+addition, a remap configuration can use ``@PPARAM`` to set one of these slot
variables explicitly
+as part of the configuration.
Groups
------
diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst
b/doc/admin-guide/plugins/header_rewrite.en.rst
index e7f8c74769..5ec2e89f20 100644
--- a/doc/admin-guide/plugins/header_rewrite.en.rst
+++ b/doc/admin-guide/plugins/header_rewrite.en.rst
@@ -153,9 +153,19 @@ like the following::
Which converts any 4xx HTTP status code from the origin server to a 404. A
response from the origin with a status of 200 would be unaffected by this rule.
+Advanced Conditionals
+---------------------
+
+The header_rewrite plugin supports advanced conditional logic that allows
+for more sophisticated rule construction, including branching logic, nested
+conditionals, and complex boolean expressions.
+
+else and elif Clauses
+~~~~~~~~~~~~~~~~~~~~~
+
An optional ``else`` clause may be specified, which will be executed if the
-conditions are not met. The ``else`` clause is specified by starting a new line
-with the word ``else``. The following example illustrates this::
+conditions are not met. The ``else`` clause is specified by starting a new
+line with the word ``else``. The following example illustrates this::
cond %{STATUS} >399 [AND]
cond %{STATUS} <500
@@ -164,10 +174,12 @@ with the word ``else``. The following example illustrates
this::
set-status 503
The ``else`` clause is not a condition, and does not take any flags, it is
-of course optional, but when specified must be followed by at least one
operator.
+of course optional, but when specified must be followed by at least one
+operator.
-You can also do an ``elif`` (else if) clause, which is specified by starting a
new line
-with the word ``elif``. The following example illustrates this::
+You can also do an ``elif`` (else if) clause, which is specified by
+starting a new line with the word ``elif``. The following example
+illustrates this::
cond %{STATUS} >399 [AND]
cond %{STATUS} <500
@@ -178,14 +190,105 @@ with the word ``elif``. The following example
illustrates this::
else
set-status 503
-Keep in mind that nesting the ``else`` and ``elif`` clauses is not allowed,
but any
-number of ``elif`` clauses can be specified. We can consider these clauses are
more
-powerful and flexible ``switch`` statement. In an ``if-elif-else`` rule, only
one
-will evaluate its operators.
+Any number of ``elif`` clauses can be specified. We can consider these
+clauses are more powerful and flexible ``switch`` statement. In an
+``if-elif-else`` rule, only one will evaluate its operators.
+
+Note that while ``else`` and ``elif`` themselves cannot be directly nested,
+you can use ``if``/``endif`` blocks within ``else`` or ``elif`` operator
+sections to achieve nested conditional logic (see `Nested Conditionals with
+if/endif`_).
Similarly, each ``else`` and ``elif`` have the same implied
:ref:`Hook Condition <hook_conditions>` as the initial condition.
+Nested Conditionals with if/endif
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For more complex logic requiring nested conditionals, the ``if`` and
+``endif`` pseudo-operators can be used. While ``else`` and ``elif``
+themselves cannot be directly nested, you can use ``if``/``endif`` blocks
+within any operator section (including inside ``else`` or ``elif`` blocks)
+to achieve arbitrary nesting depth.
+
+The ``if`` operator starts a new conditional block, and ``endif`` closes
+it. Each ``if`` must have a matching ``endif``. Here's an example::
+
+ cond %{READ_RESPONSE_HDR_HOOK} [AND]
+ cond %{STATUS} >399
+ if
+ cond %{HEADER:X-Custom-Error} ="true"
+ set-header X-Error-Handled "yes"
+ else
+ set-header X-Error-Handled "no"
+ endif
+ set-status 500
+
+In this example, the nested ``if``/``endif`` block is only evaluated when
+the status is greater than 399. The nested block itself can contain
+``else`` or ``elif`` clauses, and you can nest multiple levels deep::
+
+ cond %{READ_RESPONSE_HDR_HOOK}
+ if
+ cond %{STATUS} =404
+ if
+ cond %{CLIENT-HEADER:User-Agent} /mobile/
+ set-header X-Error-Type "mobile-404"
+ else
+ set-header X-Error-Type "desktop-404"
+ endif
+ elif
+ cond %{STATUS} =500
+ set-header X-Error-Type "server-error"
+ endif
+
+GROUP Conditions in Advanced Conditionals
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The `GROUP`_ condition can be combined with advanced conditionals to
+create very sophisticated boolean expressions. ``GROUP`` conditions act as
+parentheses in your conditional logic, allowing you to mix AND, OR, and NOT
+operators in complex ways.
+
+Here's an example combining ``GROUP`` with ``if``/``endif``::
+
+ cond %{READ_RESPONSE_HDR_HOOK} [AND]
+ cond %{STATUS} >399
+ if
+ cond %{GROUP} [OR]
+ cond %{CLIENT-HEADER:X-Retry} ="true" [AND]
+ cond %{METHOD} =GET
+ cond %{GROUP:END}
+ cond %{CLIENT-HEADER:X-Force-Cache} ="" [NOT]
+ set-header X-Can-Retry "yes"
+ else
+ set-header X-Can-Retry "no"
+ endif
+ set-status 500
+
+This creates the logic: if error status, then set retry header when
+``((X-Retry=true AND METHOD=GET) OR X-Force-Cache header exists)``.
+The GROUP is necessary here to properly combine the two conditions with OR.
+
+You can also use ``GROUP`` with ``else`` and ``elif`` inside nested
conditionals::
+
+ cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+ cond %{STATUS} >399
+ if
+ cond %{GROUP} [OR]
+ cond %{HEADER:X-Custom} ="retry" [AND]
+ cond %{METHOD} =POST
+ cond %{GROUP:END}
+ cond %{HEADER:Content-Type} /json/
+ set-header X-Error-Handler "json-retry"
+ elif
+ cond %{METHOD} =GET
+ set-header X-Error-Handler "get-error"
+ else
+ set-header X-Error-Handler "standard"
+ endif
+ set-status 500
+
State variables
---------------
@@ -923,6 +1026,29 @@ no facility to increment by other amounts, nor is it
possible to initialize the
counter with any value other than ``0``. Additionally, the counter will reset
whenever |TS| is restarted.
+if
+~~
+::
+
+ if
+ <conditions>
+ <operators>
+ endif
+
+This is a pseudo-operator that enables nested conditional blocks within
+the operator section of a rule. While ``else`` and ``elif`` themselves
+cannot be directly nested, you can use ``if``/``endif`` blocks within any
+operator section (including inside ``else`` or ``elif`` blocks) to create
+arbitrary nesting depth for complex conditional logic.
+
+The ``if`` operator must be preceded by conditions and followed by at
+least one condition or operator. Each ``if`` must have a matching
+``endif`` to close the block. Within an ``if``/``endif`` block, you can
+use regular conditions, operators, and even ``else`` and ``elif`` clauses.
+
+For detailed usage and examples, see `Nested Conditionals with if/endif`_
+in the `Advanced Conditionals`_ section.
+
no-op
~~~~~
::
diff --git a/plugins/header_rewrite/factory.cc
b/plugins/header_rewrite/factory.cc
index 9e0499f060..9f5f86083c 100644
--- a/plugins/header_rewrite/factory.cc
+++ b/plugins/header_rewrite/factory.cc
@@ -92,6 +92,7 @@ operator_factory(const std::string &op)
} else if (op == "set-next-hop-strategy") {
o = new OperatorSetNextHopStrategy();
} else {
+ // Note that we don't support the OperatorIf() pseudo-operator here!
TSError("[%s] Unknown operator: %s", PLUGIN_NAME, op.c_str());
return nullptr;
}
diff --git a/plugins/header_rewrite/header_rewrite.cc
b/plugins/header_rewrite/header_rewrite.cc
index 89fe252e99..61895124f6 100644
--- a/plugins/header_rewrite/header_rewrite.cc
+++ b/plugins/header_rewrite/header_rewrite.cc
@@ -36,6 +36,7 @@
#include "resources.h"
#include "conditions.h"
#include "conditions_geo.h"
+#include "operators.h"
// Debugs
namespace header_rewrite_ns
@@ -145,15 +146,14 @@ validate_rule_completion(RuleSet *rule, const std::string
&fname, int lineno)
switch (rule->get_clause()) {
case Parser::CondClause::ELIF:
- if (!rule->cur_section()->group.has_conditions() ||
!rule->cur_section()->has_operator()) {
- TSError("[%s] ELIF clause must have both conditions and operators in
file: %s, lineno: %d", PLUGIN_NAME, fname.c_str(),
- lineno);
+ if (!rule->section_has_condition() || !rule->section_has_operator()) {
+ TSError("[%s] ELIF conditions without operators are not allowed in file:
%s, lineno: %d", PLUGIN_NAME, fname.c_str(), lineno);
return false;
}
break;
case Parser::CondClause::ELSE:
- if (rule->cur_section()->group.has_conditions()) {
+ if (rule->section_has_condition()) {
TSError("[%s] conditions not allowed in ELSE clause in file: %s, lineno:
%d", PLUGIN_NAME, fname.c_str(), lineno);
return false;
}
@@ -168,6 +168,11 @@ validate_rule_completion(RuleSet *rule, const std::string
&fname, int lineno)
case Parser::CondClause::COND:
break;
+
+ case Parser::CondClause::IF:
+ case Parser::CondClause::ENDIF:
+ // IF and ENDIF are handled separately in the main parsing loop
+ break;
}
return true;
@@ -185,8 +190,11 @@ RulesConfig::parse_config(const std::string &fname,
TSHttpHookID default_hook, c
std::unique_ptr<RuleSet> rule(nullptr);
std::string filename;
int lineno = 0;
+ ConditionGroup *group = nullptr;
std::stack<ConditionGroup *> group_stack;
- ConditionGroup *group = nullptr;
+ std::stack<OperatorIf *> if_stack;
+
+ constexpr int MAX_IF_NESTING_DEPTH = 10;
if (0 == fname.size()) {
TSError("[%s] no config filename provided", PLUGIN_NAME);
@@ -246,7 +254,11 @@ RulesConfig::parse_config(const std::string &fname,
TSHttpHookID default_hook, c
// Deal with the elif / else special keywords, these are neither
conditions nor operators.
if (p.is_else() || p.is_elif()) {
Dbg(pi_dbg_ctl, "Entering elif/else, CondClause=%d",
static_cast<int>(p.get_clause()));
- if (rule) {
+
+ if (!if_stack.empty()) {
+ group = if_stack.top()->new_section(p.get_clause());
+ continue;
+ } else if (rule) {
group = rule->new_section(p.get_clause());
continue;
} else {
@@ -256,8 +268,7 @@ RulesConfig::parse_config(const std::string &fname,
TSHttpHookID default_hook, c
}
// If we are at the beginning of a new condition, save away the previous
rule (but only if it has operators).
- // This also has to deal with the fact that we allow implicit hooks to end
/ start a new rule.
- if (p.is_cond() && rule && (is_hook ||
rule->cur_section()->has_operator())) {
+ if (p.is_cond() && rule && if_stack.empty() && (is_hook ||
rule->section_has_operator())) {
if (!validate_rule_completion(rule.get(), fname, lineno)) {
return false;
} else {
@@ -299,7 +310,13 @@ RulesConfig::parse_config(const std::string &fname,
TSHttpHookID default_hook, c
// Long term, maybe we need to percolate all this up through
add_condition() / add_operator() rather than this big ugly try.
try {
if (p.is_cond()) {
- Condition *cond = rule->make_condition(p, filename.c_str(), lineno);
+ Condition *cond = nullptr;
+
+ if (!if_stack.empty()) {
+ cond = if_stack.top()->make_condition(p, filename.c_str(), lineno);
+ } else {
+ cond = rule->make_condition(p, filename.c_str(), lineno);
+ }
if (!cond) {
throw std::runtime_error("add_condition() failed");
@@ -327,9 +344,54 @@ RulesConfig::parse_config(const std::string &fname,
TSHttpHookID default_hook, c
group->add_condition(cond);
}
}
- } else { // Operator
- if (!rule->add_operator(p, filename.c_str(), lineno)) {
- throw std::runtime_error("add_operator() failed");
+ } else {
+ if (p.is_if()) {
+ if (if_stack.size() >= MAX_IF_NESTING_DEPTH) {
+ throw std::runtime_error("maximum if nesting depth exceeded");
+ }
+
+ auto *op_if = new OperatorIf();
+
+ if_stack.push(op_if);
+ group = op_if->get_group(); // Set group to the new OperatorIf's
group
+ Dbg(dbg_ctl, "Started nested OperatorIf, depth: %zu",
if_stack.size());
+
+ } else if (p.is_endif()) {
+ if (if_stack.empty()) {
+ throw std::runtime_error("endif without matching if");
+ }
+
+ OperatorIf *op_if = if_stack.top();
+
+ if_stack.pop();
+ if (!if_stack.empty()) {
+ auto *parent_sec = if_stack.top()->cur_section();
+
+ if (parent_sec->ops.oper) {
+ parent_sec->ops.oper->append(op_if);
+ } else {
+ parent_sec->ops.oper.reset(op_if);
+ }
+ group = if_stack.top()->get_group();
+ } else {
+ if (!rule->add_operator(op_if)) {
+ delete op_if;
+ throw std::runtime_error("Failed to add nested OperatorIf to
RuleSet");
+ }
+ group = rule->get_group();
+ }
+ Dbg(dbg_ctl, "Completed nested OperatorIf, depth now: %zu",
if_stack.size());
+
+ } else {
+ if (!if_stack.empty()) {
+ if (!if_stack.top()->add_operator(p, filename.c_str(), lineno)) {
+ throw std::runtime_error("add_operator() failed in nested
OperatorIf");
+ }
+ } else {
+ if (!rule->add_operator(p, filename.c_str(), lineno)) {
+ throw std::runtime_error("add_operator() failed");
+ }
+ }
}
}
} catch (std::runtime_error &e) {
@@ -353,6 +415,16 @@ RulesConfig::parse_config(const std::string &fname,
TSHttpHookID default_hook, c
return false;
}
+ // Check for unmatched if statements
+ if (!if_stack.empty()) {
+ TSError("[%s] %zu unmatched 'if' statement(s) without 'endif' in file:
%s", PLUGIN_NAME, if_stack.size(), fname.c_str());
+ while (!if_stack.empty()) {
+ delete if_stack.top();
+ if_stack.pop();
+ }
+ return false;
+ }
+
// Add the last rule (possibly the only rule)
if (rule) {
if (!validate_rule_completion(rule.get(), fname, lineno)) {
@@ -434,10 +506,8 @@ cont_rewrite_headers(TSCont contp, TSEvent event, void
*edata)
// Get the resources necessary to process this event
res.gather(conf->resid(hook), hook);
- // Evaluation of all rules. This code is sort of duplicate in DoRemap as
well.
while (rule) {
- const RuleSet::OperatorAndMods &ops = rule->eval(res);
- const OperModifiers rt = rule->exec(ops, res);
+ const OperModifiers rt = rule->exec(res);
if (rt & OPER_NO_REENABLE) {
reenable = false;
@@ -703,8 +773,7 @@ TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo
*rri)
res.gather(conf->resid(TS_REMAP_PSEUDO_HOOK), TS_REMAP_PSEUDO_HOOK);
do {
- const RuleSet::OperatorAndMods &ops = rule->eval(res);
- const OperModifiers rt = rule->exec(ops, res);
+ const OperModifiers rt = rule->exec(res);
ink_assert((rt & OPER_NO_REENABLE) == 0);
diff --git a/plugins/header_rewrite/operator.h
b/plugins/header_rewrite/operator.h
index 2d54f6fad4..bc23c93aab 100644
--- a/plugins/header_rewrite/operator.h
+++ b/plugins/header_rewrite/operator.h
@@ -22,6 +22,7 @@
#pragma once
#include <string>
+#include <memory>
#include "ts/ts.h"
@@ -39,6 +40,20 @@ enum OperModifiers {
OPER_NO_REENABLE = 16,
};
+// Forward declaration
+class Operator;
+
+// Holding the operator and mods - used by both RuleSet and OperatorIf
+struct OperatorAndMods {
+ OperatorAndMods() = default;
+
+ OperatorAndMods(const OperatorAndMods &) = delete;
+ OperatorAndMods &operator=(const OperatorAndMods &) = delete;
+
+ std::unique_ptr<Operator> oper;
+ OperModifiers oper_mods = OPER_NONE;
+};
+
///////////////////////////////////////////////////////////////////////////////
// Base class for all Operators (this is also the interface)
//
diff --git a/plugins/header_rewrite/operators.cc
b/plugins/header_rewrite/operators.cc
index ac7b750440..c8c4f66b95 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -30,6 +30,9 @@
#include "operators.h"
#include "ts/apidefs.h"
+#include "conditions.h"
+#include "factory.h"
+#include "ruleset.h"
namespace
{
@@ -1677,3 +1680,123 @@ OperatorSetNextHopStrategy::exec(const Resources &res)
const
return true;
}
+
+///////////////////////////////////////////////////////////////////////////////
+// OperatorIf class implementations
+// Keep this at the end of the files, since this is not really an Operator.
+//
+ConditionGroup *
+OperatorIf::new_section(Parser::CondClause clause)
+{
+ TSAssert(_cur_section && !_cur_section->next);
+
+ _clause = clause;
+ _cur_section->next = std::make_unique<CondOpSection>();
+ _cur_section = _cur_section->next.get();
+
+ return &_cur_section->group;
+}
+
+bool
+OperatorIf::add_operator(Parser &p, const char *filename, int lineno)
+{
+ Operator *op = operator_factory(p.get_op());
+
+ if (!op) {
+ TSError("[%s] Unknown operator: %s, file: %s, line: %d", PLUGIN_NAME,
p.get_op().c_str(), filename, lineno);
+ return false;
+ }
+
+ Dbg(pi_dbg_ctl, " Adding operator: %s(%s)=\"%s\"", p.get_op().c_str(),
p.get_arg().c_str(), p.get_value().c_str());
+
+ try {
+ op->initialize(p);
+ } catch (std::exception const &ex) {
+ delete op;
+ TSError("[%s] Failed to initialize operator: %s, file: %s, line: %d,
error: %s", PLUGIN_NAME, p.get_op().c_str(), filename,
+ lineno, ex.what());
+ return false;
+ }
+
+ // Add to current section
+ if (_cur_section->ops.oper) {
+ _cur_section->ops.oper->append(op);
+ } else {
+ _cur_section->ops.oper.reset(op);
+ _cur_section->ops.oper_mods = op->get_oper_modifiers();
+ }
+
+ return true;
+}
+
+Condition *
+OperatorIf::make_condition(Parser &p, const char *filename, int lineno)
+{
+ Condition *cond = condition_factory(p.get_op());
+
+ if (!cond) {
+ TSError("[%s] Unknown condition: %s, file: %s, line: %d", PLUGIN_NAME,
p.get_op().c_str(), filename, lineno);
+ return nullptr;
+ }
+
+ Dbg(pi_dbg_ctl, " Creating condition: %%{%s} with arg: %s",
p.get_op().c_str(), p.get_arg().c_str());
+
+ try {
+ cond->initialize(p);
+ } catch (std::exception const &ex) {
+ delete cond;
+ TSError("[%s] Failed to initialize condition: %s, file: %s, line: %d,
error: %s", PLUGIN_NAME, p.get_op().c_str(), filename,
+ lineno, ex.what());
+ return nullptr;
+ }
+
+ return cond;
+}
+
+bool
+OperatorIf::has_operator() const
+{
+ const CondOpSection *section = &_sections;
+
+ while (section != nullptr) {
+ if (section->has_operator()) {
+ return true;
+ }
+ section = section->next.get();
+ }
+ return false;
+}
+
+OperModifiers
+OperatorIf::exec_and_return_mods(const Resources &res) const
+{
+ Dbg(dbg_ctl, "Executing OperatorIf");
+
+ // Go through each section (if/elif/else) until one matches
+ for (auto *section = const_cast<CondOpSection *>(&_sections); section !=
nullptr; section = section->next.get()) {
+ if (section->group.eval(res)) {
+ Dbg(dbg_ctl, "OperatorIf section condition matched, executing
operators");
+ return exec_section(section, res);
+ }
+ }
+
+ Dbg(dbg_ctl, "OperatorIf: no section matched");
+ return OPER_NONE;
+}
+
+OperModifiers
+OperatorIf::exec_section(const CondOpSection *section, const Resources &res)
const
+{
+ if (nullptr == section->ops.oper) {
+ return section->ops.oper_mods;
+ }
+
+ auto no_reenable_count = section->ops.oper->do_exec(res);
+
+ ink_assert(no_reenable_count < 2);
+ if (no_reenable_count) {
+ return static_cast<OperModifiers>(section->ops.oper_mods |
OPER_NO_REENABLE);
+ }
+
+ return section->ops.oper_mods;
+}
diff --git a/plugins/header_rewrite/operators.h
b/plugins/header_rewrite/operators.h
index a0b825d3d0..179bd799a0 100644
--- a/plugins/header_rewrite/operators.h
+++ b/plugins/header_rewrite/operators.h
@@ -22,6 +22,7 @@
#pragma once
#include <string>
+#include <memory>
#include "ts/ts.h"
@@ -29,6 +30,12 @@
#include "resources.h"
#include "value.h"
+// Forward declarations
+class Parser;
+
+// Full includes needed for member variables
+#include "conditions.h"
+
///////////////////////////////////////////////////////////////////////////////
// Operator declarations.
//
@@ -670,3 +677,76 @@ protected:
private:
Value _value;
};
+
+///////////////////////////////////////////////////////////////////////////////
+// OperatorIf class - implements nested if/elif/else as a pseudo-operator.
+// Keep this at the end of the files, since this is not really an Operator.
+//
+class OperatorIf : public Operator
+{
+public:
+ struct CondOpSection {
+ CondOpSection() = default;
+
+ ~CondOpSection() = default;
+
+ CondOpSection(const CondOpSection &) = delete;
+ CondOpSection &operator=(const CondOpSection &) = delete;
+
+ bool
+ has_operator() const
+ {
+ return ops.oper != nullptr;
+ }
+
+ ConditionGroup group;
+ OperatorAndMods ops;
+ std::unique_ptr<CondOpSection> next; // For elif/else sections
+ };
+
+ OperatorIf() { Dbg(dbg_ctl, "Calling CTOR for OperatorIf"); }
+
+ // noncopyable
+ OperatorIf(const OperatorIf &) = delete;
+ void operator=(const OperatorIf &) = delete;
+
+ ConditionGroup *new_section(Parser::CondClause clause);
+ bool add_operator(Parser &p, const char *filename, int lineno);
+ Condition *make_condition(Parser &p, const char *filename, int lineno);
+ bool has_operator() const;
+
+ ConditionGroup *
+ get_group()
+ {
+ return &_cur_section->group;
+ }
+
+ Parser::CondClause
+ get_clause() const
+ {
+ return _clause;
+ }
+
+ CondOpSection *
+ cur_section() const
+ {
+ return _cur_section;
+ }
+
+ OperModifiers exec_and_return_mods(const Resources &res) const;
+
+protected:
+ bool
+ exec(const Resources &res) const override
+ {
+ OperModifiers mods = exec_and_return_mods(res);
+ return !(mods & OPER_NO_REENABLE);
+ }
+
+private:
+ OperModifiers exec_section(const CondOpSection *section, const Resources
&res) const;
+
+ CondOpSection _sections;
+ CondOpSection *_cur_section = &_sections;
+ Parser::CondClause _clause = Parser::CondClause::COND;
+};
diff --git a/plugins/header_rewrite/parser.cc b/plugins/header_rewrite/parser.cc
index a0f93afb77..12f0c15d85 100644
--- a/plugins/header_rewrite/parser.cc
+++ b/plugins/header_rewrite/parser.cc
@@ -15,6 +15,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
+
//////////////////////////////////////////////////////////////////////////////////////////////
// parser.cc: implementation of the config parser
//
@@ -205,6 +206,12 @@ Parser::preprocess(std::vector<std::string> tokens)
} else if (tokens[0] == "elif") {
_clause = CondClause::ELIF;
return true;
+ } else if (tokens[0] == "if") {
+ _clause = CondClause::IF;
+ return true;
+ } else if (tokens[0] == "endif") {
+ _clause = CondClause::ENDIF;
+ return true;
}
// Is it a condition or operator?
diff --git a/plugins/header_rewrite/parser.h b/plugins/header_rewrite/parser.h
index 9844ad0899..634df4694b 100644
--- a/plugins/header_rewrite/parser.h
+++ b/plugins/header_rewrite/parser.h
@@ -113,7 +113,7 @@ std::optional<ConfReader> openConfig(const std::string
&filename);
class Parser
{
public:
- enum class CondClause { OPER, COND, ELIF, ELSE };
+ enum class CondClause { OPER, COND, ELIF, ELSE, IF, ENDIF };
Parser() = default; // No from/to URLs for this parser
Parser(char *from_url, char *to_url) : _from_url(from_url), _to_url(to_url)
{}
@@ -165,6 +165,18 @@ public:
return _clause == CondClause::ELIF;
}
+ bool
+ is_if() const
+ {
+ return _clause == CondClause::IF;
+ }
+
+ bool
+ is_endif() const
+ {
+ return _clause == CondClause::ENDIF;
+ }
+
const std::string &
get_op() const
{
diff --git a/plugins/header_rewrite/ruleset.cc
b/plugins/header_rewrite/ruleset.cc
index 50d16ac4c4..28fda37a51 100644
--- a/plugins/header_rewrite/ruleset.cc
+++ b/plugins/header_rewrite/ruleset.cc
@@ -23,10 +23,24 @@
#include "ruleset.h"
#include "factory.h"
+#include "operators.h"
+
+RuleSet::RuleSet()
+{
+ Dbg(dbg_ctl, "RuleSet CTOR");
+}
+
+RuleSet::~RuleSet()
+{
+ Dbg(dbg_ctl, "RulesSet DTOR");
+}
+
+OperModifiers
+RuleSet::exec(const Resources &res) const
+{
+ return _op_if.exec_and_return_mods(res);
+}
-///////////////////////////////////////////////////////////////////////////////
-// Class implementation (no reason to have these inline)
-//
void
RuleSet::append(std::unique_ptr<RuleSet> rule)
{
@@ -47,7 +61,7 @@ RuleSet::make_condition(Parser &p, const char *filename, int
lineno)
Condition *c = condition_factory(p.get_op());
if (nullptr == c) {
- return nullptr; // Complete failure in the factory
+ return nullptr;
}
Dbg(pi_dbg_ctl, " Creating condition: %%{%s} with arg: %s",
p.get_op().c_str(), p.get_arg().c_str());
@@ -65,7 +79,6 @@ RuleSet::make_condition(Parser &p, const char *filename, int
lineno)
return nullptr;
}
- // Update some ruleset state based on this new condition;
_last |= c->last();
_ids = static_cast<ResourceIDs>(_ids | c->get_resource_ids());
@@ -89,17 +102,16 @@ RuleSet::add_operator(Parser &p, const char *filename, int
lineno)
return false;
}
- OperatorAndMods &ops = _cur_section->ops;
+ auto *cur_sec = _op_if.cur_section();
- if (!ops.oper) {
- ops.oper = op;
+ if (!cur_sec->ops.oper) {
+ cur_sec->ops.oper.reset(op);
} else {
- ops.oper->append(op);
+ cur_sec->ops.oper->append(op);
}
- // Update some ruleset state based on this new operator
- ops.oper_mods = static_cast<OperModifiers>(ops.oper_mods |
ops.oper->get_oper_modifiers());
- _ids = static_cast<ResourceIDs>(_ids |
ops.oper->get_resource_ids());
+ cur_sec->ops.oper_mods = static_cast<OperModifiers>(cur_sec->ops.oper_mods
| cur_sec->ops.oper->get_oper_modifiers());
+ _ids = static_cast<ResourceIDs>(_ids |
cur_sec->ops.oper->get_resource_ids());
return true;
}
@@ -120,3 +132,21 @@ RuleSet::get_all_resource_ids() const
return ids;
}
+
+bool
+RuleSet::add_operator(Operator *op)
+{
+ auto *cur_sec = _op_if.cur_section();
+
+ if (!cur_sec->ops.oper) {
+ cur_sec->ops.oper.reset(op);
+ } else {
+ cur_sec->ops.oper->append(op);
+ }
+
+ // Update some ruleset state based on this new operator
+ cur_sec->ops.oper_mods = static_cast<OperModifiers>(cur_sec->ops.oper_mods |
cur_sec->ops.oper->get_oper_modifiers());
+ _ids = static_cast<ResourceIDs>(_ids |
cur_sec->ops.oper->get_resource_ids());
+
+ return true;
+}
diff --git a/plugins/header_rewrite/ruleset.h b/plugins/header_rewrite/ruleset.h
index 4a5887aab9..78680bc9d7 100644
--- a/plugins/header_rewrite/ruleset.h
+++ b/plugins/header_rewrite/ruleset.h
@@ -30,115 +30,75 @@
#include "resources.h"
#include "parser.h"
#include "conditions.h"
+#include "operators.h"
///////////////////////////////////////////////////////////////////////////////
-// Class holding one ruleset. A ruleset is one (or more) pre-conditions, and
-// one (or more) operators.
+// RuleSet: Represents a complete wrapping a single OperatorIf.
//
class RuleSet
{
public:
- // Holding the IF and ELSE operators and mods, in two separate linked lists.
- struct OperatorAndMods {
- OperatorAndMods() = default;
-
- OperatorAndMods(const OperatorAndMods &) = delete;
- OperatorAndMods &operator=(const OperatorAndMods &) = delete;
-
- Operator *oper = nullptr;
- OperModifiers oper_mods = OPER_NONE;
- };
-
- struct CondOpSection {
- CondOpSection() = default;
-
- ~CondOpSection()
- {
- delete ops.oper;
- delete next;
- }
-
- CondOpSection(const CondOpSection &) = delete;
- CondOpSection &operator=(const CondOpSection &) = delete;
-
- bool
- has_operator() const
- {
- return ops.oper != nullptr;
- }
-
- ConditionGroup group;
- OperatorAndMods ops;
- CondOpSection *next = nullptr; // For elif / else sections.
- };
-
- RuleSet() { Dbg(dbg_ctl, "RuleSet CTOR"); }
-
- ~RuleSet() { Dbg(dbg_ctl, "RulesSet DTOR"); }
+ RuleSet();
+ ~RuleSet();
// noncopyable
RuleSet(const RuleSet &) = delete;
void operator=(const RuleSet &) = delete;
- // No reason to inline these
void append(std::unique_ptr<RuleSet> rule);
Condition *make_condition(Parser &p, const char *filename, int lineno);
- bool add_operator(Parser &p, const char *filename, int lineno);
ResourceIDs get_all_resource_ids() const;
+ bool add_operator(Parser &p, const char *filename, int lineno);
+ bool add_operator(Operator *op);
- bool
- has_operator() const
+ ConditionGroup *
+ get_group()
{
- const CondOpSection *section = &_sections;
-
- while (section != nullptr) {
- if (section->has_operator()) {
- return true;
- }
- section = section->next;
- }
- return false;
+ return _op_if.get_group();
}
- void
- set_hook(TSHttpHookID hook)
+ Parser::CondClause
+ get_clause() const
{
- _hook = hook;
+ return _op_if.get_clause();
}
ConditionGroup *
- get_group()
+ new_section(Parser::CondClause clause)
{
- return &_cur_section->group;
+ return _op_if.new_section(clause);
}
- TSHttpHookID
- get_hook() const
+ bool
+ has_operator() const
{
- return _hook;
+ return _op_if.has_operator();
}
- Parser::CondClause
- get_clause() const
+ bool
+ section_has_condition() const
{
- return _clause;
+ auto *sec = _op_if.cur_section();
+ return sec ? sec->group.has_conditions() : false;
}
- CondOpSection *
- cur_section()
+ bool
+ section_has_operator() const
{
- return _cur_section;
+ auto *sec = _op_if.cur_section();
+ return sec ? sec->has_operator() : false;
}
- ConditionGroup *
- new_section(Parser::CondClause clause)
+ void
+ set_hook(TSHttpHookID hook)
{
- TSAssert(_cur_section && !_cur_section->next);
- _clause = clause;
- _cur_section->next = new CondOpSection();
- _cur_section = _cur_section->next;
+ _hook = hook;
+ }
- return &_cur_section->group;
+ TSHttpHookID
+ get_hook() const
+ {
+ return _hook;
}
ResourceIDs
@@ -153,49 +113,14 @@ public:
return _last;
}
- OperModifiers
- exec(const OperatorAndMods &ops, const Resources &res) const
- {
- if (nullptr == ops.oper) {
- return ops.oper_mods;
- }
-
- auto no_reenable_count = ops.oper->do_exec(res);
-
- ink_assert(no_reenable_count < 2);
- if (no_reenable_count) {
- return static_cast<OperModifiers>(ops.oper_mods | OPER_NO_REENABLE);
- }
-
- return ops.oper_mods;
- }
-
- const RuleSet::OperatorAndMods &
- eval(const Resources &res)
- {
- for (CondOpSection *sec = &_sections; sec != nullptr; sec = sec->next) {
- if (sec->group.eval(res)) {
- return sec->ops;
- }
- }
-
- // No matching condition found, return empty operator set.
- static OperatorAndMods empty_ops;
- return empty_ops;
- }
+ OperModifiers exec(const Resources &res) const;
// Linked list of RuleSets
std::unique_ptr<RuleSet> next;
private:
- // This holds one condition group, and the ops and optional else_ops, there's
- // aways at least one of these in the vector (no "elif" sections).
- CondOpSection _sections;
- CondOpSection *_cur_section = &_sections;
-
- // State values (updated when conds / operators are added)
- TSHttpHookID _hook = TS_HTTP_READ_RESPONSE_HDR_HOOK; // Which hook
is this rule for
- ResourceIDs _ids = RSRC_NONE;
- bool _last = false;
- Parser::CondClause _clause = Parser::CondClause::OPER;
+ OperatorIf _op_if;
+ TSHttpHookID _hook = TS_HTTP_READ_RESPONSE_HDR_HOOK; // Which hook is this
rule for
+ ResourceIDs _ids = RSRC_NONE;
+ bool _last = false;
};
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_definitely.gold
b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_definitely.gold
new file mode 100644
index 0000000000..aaa750a26e
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_definitely.gold
@@ -0,0 +1,18 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: definitely
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Definitely
+< X-When-200-After: Yes
+``
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else.gold
b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else.gold
new file mode 100644
index 0000000000..a04b910a7d
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else.gold
@@ -0,0 +1,17 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Nothing
+< X-When-200-After: Yes
+``
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else_fie.gold
b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else_fie.gold
new file mode 100644
index 0000000000..09d4e47708
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else_fie.gold
@@ -0,0 +1,19 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Fie: fie
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Nothing
+< X-Fie-Anywhere: Yes
+< X-When-200-After: Yes
+``
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_bar.gold
b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_bar.gold
new file mode 100644
index 0000000000..b583ca387d
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_bar.gold
@@ -0,0 +1,20 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: foo
+> X-Bar: bar
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Yes
+< X-Foo-And-Bar: Yes
+< X-When-200-After: Yes
+``
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_fie.gold
b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_fie.gold
new file mode 100644
index 0000000000..126f4bdade
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_fie.gold
@@ -0,0 +1,21 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: foo
+> X-Fie: fie
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Yes
+< X-Foo-And-Fie: Yes
+< X-Fie-Anywhere: Yes
+< X-When-200-After: Yes
+``
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_maybe.gold
b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_maybe.gold
new file mode 100644
index 0000000000..f975acf4b1
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_maybe.gold
@@ -0,0 +1,18 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: maybe
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Maybe
+< X-When-200-After: Yes
+``
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py
b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py
index ed268752f3..e7ef3b691c 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py
+++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py
@@ -94,6 +94,10 @@ remap_rules = [
"from": f"{url_base}_11/",
"to": f"{origin_base}_11/",
"plugins": [("header_rewrite",
[f"{mgr.run_dir}/rule_set_body_status.conf"])]
+ }, {
+ "from": f"{url_base}_12/",
+ "to": f"{origin_base}_12/",
+ "plugins": [("header_rewrite", [f"{mgr.run_dir}/nested_ifs.conf"])]
}
]
@@ -205,6 +209,11 @@ origin_rules = [
"timestamp": "1469733493.993",
"body": "ATS should not serve this body"
}),
+ ({
+ "headers": "GET /to_12/ HTTP/1.1\r\nHost: www.example.com\r\n\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+ }, def_resp),
]
mgr.add_server_responses(origin_rules)
@@ -328,6 +337,36 @@ test_runs = [
"gold": "gold/set_body_status.gold",
"gold_stdout": "gold/set_body_status_stdout.gold",
},
+ {
+ "desc": "Nested if/elif/else - X-Foo=foo + X-Bar=bar path",
+ "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: foo" -H
"X-Bar: bar"',
+ "gold": "gold/nested_ifs_foo_bar.gold",
+ },
+ {
+ "desc": "Nested if/elif/else - X-Foo=foo + X-Fie=fie path",
+ "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: foo" -H
"X-Fie: fie"',
+ "gold": "gold/nested_ifs_foo_fie.gold",
+ },
+ {
+ "desc": "Nested if/elif/else - X-Foo=maybe path",
+ "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: maybe"',
+ "gold": "gold/nested_ifs_maybe.gold",
+ },
+ {
+ "desc": "Nested if/elif/else - X-Foo=definitely path",
+ "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: definitely"',
+ "gold": "gold/nested_ifs_definitely.gold",
+ },
+ {
+ "desc": "Nested if/elif/else - else path (no X-Foo)",
+ "curl": f'{curl_proxy} "http://{url_base}_12/"',
+ "gold": "gold/nested_ifs_else.gold",
+ },
+ {
+ "desc": "Nested if/elif/else - else path with X-Fie (tests second if)",
+ "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Fie: fie"',
+ "gold": "gold/nested_ifs_else_fie.gold",
+ },
]
mgr.execute_tests(test_runs)
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
b/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
index 0f909f1046..c957bfa7eb 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
+++ b/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
@@ -27,8 +27,6 @@ elif
else
set-header X-Response-Foo "No"
-# ToDo: This should use the implicit hook of %{REMAP_PSEUDO_HOOK}, needs #12557
-cond %{REMAP_PSEUDO_HOOK} [AND]
cond %{CLIENT-HEADER:X-Fie} ="fie" [NOCASE]
add-header X-Client-Foo "Yes"
elif
diff --git
a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf
similarity index 54%
copy from tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
copy to tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf
index 0f909f1046..5453b0b6fa 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
+++ b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf
@@ -14,23 +14,31 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-
+#
cond %{SEND_RESPONSE_HDR_HOOK} [AND]
-cond %{CLIENT-HEADER:X-Client-Foo} ="foo" [NOCASE,PRE]
- set-header X-Response-Foo "Prefix"
-elif
- cond %{CLIENT-HEADER:X-Client-Foo} ="bar"
- set-header X-Response-Foo "Never"
-elif
- cond %{CLIENT-HEADER:X-Client-Foo} ="" [NOT]
- set-header X-Response-Foo "Yes"
-else
- set-header X-Response-Foo "No"
-
-# ToDo: This should use the implicit hook of %{REMAP_PSEUDO_HOOK}, needs #12557
-cond %{REMAP_PSEUDO_HOOK} [AND]
-cond %{CLIENT-HEADER:X-Fie} ="fie" [NOCASE]
- add-header X-Client-Foo "Yes"
-elif
- cond %{CLIENT-HEADER:X-Fie} ="nope"
- add-header X-client-Foo "Yes"
+cond %{STATUS} =200
+ set-header X-When-200-Before "Yes"
+ if
+ cond %{CLIENT-HEADER:X-Foo} ="foo"
+ set-header X-Foo "Yes"
+ if
+ cond %{CLIENT-HEADER:X-Bar} ="bar" [NOCASE]
+ set-header X-Foo-And-Bar "Yes"
+ elif
+ cond %{CLIENT-HEADER:X-Fie} ="fie" [NOCASE]
+ set-header X-Foo-And-Fie "Yes"
+ endif
+ elif
+ cond %{CLIENT-HEADER:X-Foo} ="maybe"
+ set-header X-Foo "Maybe"
+ elif
+ cond %{CLIENT-HEADER:X-Foo} ="definitely"
+ set-header X-Foo "Definitely"
+ else
+ set-header X-Foo "Nothing"
+ endif
+ if
+ cond %{CLIENT-HEADER:X-Fie} ="fie" [NOCASE]
+ set-header X-Fie-Anywhere "Yes"
+ endif
+ set-header X-When-200-After "Yes"
diff --git a/tools/hrw4u/grammar/hrw4u.g4 b/tools/hrw4u/grammar/hrw4u.g4
index a91e314336..5db49f51be 100644
--- a/tools/hrw4u/grammar/hrw4u.g4
+++ b/tools/hrw4u/grammar/hrw4u.g4
@@ -80,6 +80,7 @@ NOT_TILDE : '!~';
COLON : ':';
COMMA : ',';
SEMICOLON : ';';
+AT : '@';
COMMENT : '#' ~[\r\n]* ;
WS : [ \t\r\n]+ -> skip ;
@@ -121,7 +122,7 @@ variablesItem
;
variableDecl
- : name=IDENT COLON typeName=IDENT SEMICOLON
+ : name=IDENT COLON typeName=IDENT (AT slot=NUMBER)? SEMICOLON
;
statement
@@ -156,6 +157,7 @@ block
blockItem
: statement
+ | conditional
| commentLine
;
diff --git a/tools/hrw4u/grammar/u4wrh.g4 b/tools/hrw4u/grammar/u4wrh.g4
index a88b2c585c..53972889ee 100644
--- a/tools/hrw4u/grammar/u4wrh.g4
+++ b/tools/hrw4u/grammar/u4wrh.g4
@@ -21,6 +21,8 @@ grammar u4wrh;
// Lexer Rules
// -----------------------------
COND : 'cond';
+IF_OP : 'if';
+ENDIF_OP : 'endif';
ELIF : 'elif';
ELSE : 'else';
AND_MOD : 'AND';
@@ -48,24 +50,24 @@ fragment HEXDIGIT : [0-9a-fA-F];
// Percent blocks - treat entire %{...} as one token
PERCENT_BLOCK : '%{' ~[}\r\n]* '}' '}'?;
-IDENT : [@a-zA-Z_][[email protected]]* ;
-COMPLEX_STRING : (~[ \t\r\n[\]{}(),=!><~%])+;
-NUMBER : [0-9]+ ;
-LPAREN : '(';
-RPAREN : ')';
-LBRACE : '{';
-RBRACE : '}';
-LBRACKET : '[';
-RBRACKET : ']';
-EQUALS : '=';
-NEQ : '!=';
-GT : '>';
-LT : '<';
-COMMA : ',';
-
-EOL : '\r'? '\n';
-COMMENT : '#' ~[\r\n]* ;
-WS : [ \t]+ -> skip ;
+IDENT : [@a-zA-Z_][[email protected]]* ;
+COMPLEX_STRING : (~[ \t\r\n[\]{}(),=!><~%#])+;
+NUMBER : [0-9]+ ;
+LPAREN : '(';
+RPAREN : ')';
+LBRACE : '{';
+RBRACE : '}';
+LBRACKET : '[';
+RBRACKET : ']';
+EQUALS : '=';
+NEQ : '!=';
+GT : '>';
+LT : '<';
+COMMA : ',';
+
+EOL : '\r'? '\n';
+COMMENT : '#'~[\r\n]*;
+WS : [ \t]+ -> skip ;
// -----------------------------
// Parser Rules
@@ -78,6 +80,8 @@ program
line
: condLine EOL
| opLine EOL
+ | ifLine EOL
+ | endifLine EOL
| elifLine EOL
| elseLine EOL
| commentLine EOL
@@ -88,6 +92,14 @@ condLine
: COND condBody modList?
;
+ifLine
+ : IF_OP
+ ;
+
+endifLine
+ : ENDIF_OP
+ ;
+
elifLine
: ELIF
;
diff --git a/tools/hrw4u/pyproject.toml b/tools/hrw4u/pyproject.toml
index 5a85df7981..f66a9ada00 100644
--- a/tools/hrw4u/pyproject.toml
+++ b/tools/hrw4u/pyproject.toml
@@ -20,7 +20,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hrw4u"
-version = "1.3.4"
+version = "1.4.0"
description = "HRW4U CLI tool for Apache Traffic Server header rewrite rules"
authors = [
{name = "Leif Hedstrom", email = "[email protected]"}
diff --git a/tools/hrw4u/scripts/testcase.py b/tools/hrw4u/scripts/testcase.py
index 757af95959..802e32f216 100755
--- a/tools/hrw4u/scripts/testcase.py
+++ b/tools/hrw4u/scripts/testcase.py
@@ -29,6 +29,29 @@ from hrw4u.visitor import HRW4UVisitor
KNOWN_MARKS = {"hooks", "conds", "ops", "vars", "examples", "invalid"}
+def load_exceptions(test_dir: Path) -> dict[str, str]:
+ """Load exceptions from exceptions.txt in the test directory.
+ Returns a dict mapping test filename to direction (hrw4u or u4wrh)."""
+ exceptions_file = test_dir / "exceptions.txt"
+ exceptions = {}
+
+ if not exceptions_file.exists():
+ return exceptions
+
+ for line in exceptions_file.read_text().splitlines():
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+
+ parts = line.split(':', 1)
+ if len(parts) == 2:
+ test_name = parts[0].strip()
+ direction = parts[1].strip()
+ exceptions[test_name] = direction
+
+ return exceptions
+
+
def parse_tree(input_text: str) -> tuple[hrw4uParser, any]:
stream = InputStream(input_text)
lexer = hrw4uLexer(stream)
@@ -38,12 +61,30 @@ def parse_tree(input_text: str) -> tuple[hrw4uParser, any]:
return parser, tree
-def process_file(input_path: Path, update_ast: bool = False, update_output:
bool = False, update_error: bool = False) -> bool:
+def process_file(
+ input_path: Path,
+ update_ast: bool = False,
+ update_output: bool = False,
+ update_error: bool = False,
+ exceptions: dict[str, str] = None) -> bool:
base = input_path.with_suffix('')
ast_path = base.with_suffix('.ast.txt')
output_path = base.with_suffix('.output.txt')
error_path = base.with_suffix('.error.txt')
+ # Check if this test has a direction exception
+ if exceptions is None:
+ exceptions = load_exceptions(input_path.parent)
+
+ test_filename = input_path.name.replace('.input.txt', '.input')
+ if test_filename in exceptions:
+ exception_direction = exceptions[test_filename]
+ # Skip updating for hrw4u if test is u4wrh-only (and vice versa)
+ # Since this script runs hrw4u, skip if marked as u4wrh
+ if exception_direction == 'u4wrh':
+ # This test is reverse-only, skip updating
+ return True
+
input_text = input_path.read_text()
if input_path.name.endswith(".fail.input.txt"):
@@ -90,15 +131,33 @@ def run_batch(group: str | None = None, update_ast: bool =
False, update_output:
print(f"No test files found for pattern: {base_dir}/{pattern}")
sys.exit(1)
+ # Group files by directory to load exceptions once per directory
+ files_by_dir = {}
+ for f in input_files:
+ if f.parent not in files_by_dir:
+ files_by_dir[f.parent] = []
+ files_by_dir[f.parent].append(f)
+
total = len(input_files)
failed = 0
+ skipped = 0
- for f in input_files:
- ok = process_file(f, update_ast=update_ast,
update_output=update_output, update_error=update_error)
- if not ok:
- failed += 1
+ for test_dir, files in sorted(files_by_dir.items()):
+ exceptions = load_exceptions(test_dir)
+
+ for f in files:
+ # Check if this test should be skipped
+ test_filename = f.name.replace('.input.txt', '.input')
+ if test_filename in exceptions and exceptions[test_filename] ==
'u4wrh':
+ skipped += 1
+ continue
+
+ ok = process_file(
+ f, update_ast=update_ast, update_output=update_output,
update_error=update_error, exceptions=exceptions)
+ if not ok:
+ failed += 1
- print(f"\nUpdated: {total - failed}, Failed: {failed}")
+ print(f"\nUpdated: {total - failed - skipped}, Skipped: {skipped}, Failed:
{failed}")
if failed:
sys.exit(1)
diff --git a/tools/hrw4u/src/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py
index d08c343b40..149b139dfe 100644
--- a/tools/hrw4u/src/hrw_visitor.py
+++ b/tools/hrw4u/src/hrw_visitor.py
@@ -54,8 +54,9 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
self.symbol_resolver = InverseSymbolResolver()
self._section_opened = False
- self._in_if_block = False
+ self._if_depth = 0 # Track nesting depth of if blocks
self._in_elif_mode = False
+ self._just_closed_nested = False
@lru_cache(maxsize=128)
def _cached_percent_parsing(self, pct_text: str) -> tuple[str, str | None]:
@@ -87,10 +88,10 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
with self.debug_context(f"start_section {section_type.value}"):
if self._section_opened and self._section_label == section_type:
self.debug(f"continuing existing section")
- if self._in_if_block:
+ while self._if_depth > 0:
self.decrease_indent()
self.emit("}")
- self._in_if_block = False
+ self._if_depth -= 1
self._reset_condition_state()
if self.output and self.output[-1] != "":
self.output.append("")
@@ -171,6 +172,20 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
else:
self.output.append(comment_text)
+ def visitIfLine(self, ctx: u4wrhParser.IfLineContext) -> None:
+ """Handle if operator (starts nested conditional)."""
+ with self.debug_context("visitIfLine"):
+ self._flush_pending_condition()
+ self._just_closed_nested = False
+ return None
+
+ def visitEndifLine(self, ctx: u4wrhParser.EndifLineContext) -> None:
+ """Handle endif operator (closes nested conditional)."""
+ with self.debug_context("visitEndifLine"):
+ self._close_if_block()
+ self._just_closed_nested = True
+ return None
+
def visitElifLine(self, ctx: u4wrhParser.ElifLineContext) -> None:
"""Handle elif line transitions."""
with self.debug_context("visitElifLine"):
@@ -357,10 +372,10 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
# Condition block lifecycle methods - specific to inverse visitor
def _close_if_block(self) -> None:
"""Close open if block."""
- if self._in_if_block:
+ if self._if_depth > 0:
self.decrease_indent()
self.emit("}")
- self._in_if_block = False
+ self._if_depth -= 1
def _close_section(self) -> None:
"""Close open section."""
@@ -371,7 +386,8 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
def _close_if_and_section(self) -> None:
"""Close open if blocks and sections."""
- self._close_if_block()
+ while self._if_depth > 0:
+ self._close_if_block()
self._close_section()
self._in_elif_mode = False
@@ -384,27 +400,22 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
def _start_elif_mode(self) -> None:
"""Handle elif line transitions."""
- if self._in_if_block:
+ # After endif, we need to close the parent if-statement
+ if self._if_depth > 0:
self.decrease_indent()
- self._in_if_block = False
+ self._if_depth -= 1
self._in_elif_mode = True
+ self._just_closed_nested = False
def _handle_else_transition(self) -> None:
"""Handle else line transitions."""
- if self._in_if_block:
+ if self._if_depth > 0:
self.decrease_indent()
-
- if self.output and self.output[-1].strip() == "}":
- self.output[-1] = self.format_with_indent("} else {",
self.current_indent)
- else:
- self.emit("} else {")
-
- self._in_if_block = True
- self.increase_indent()
- else:
- self.emit("else {")
- self.increase_indent()
- self._in_if_block = True
+ self._if_depth -= 1
+ self.emit("} else {")
+ self._if_depth += 1
+ self.increase_indent()
+ self._just_closed_nested = False
def _start_if_block(self, condition_expr: str) -> None:
"""Start a new if block."""
@@ -414,5 +425,5 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
else:
self.emit(f"if {condition_expr} {{")
- self._in_if_block = True
+ self._if_depth += 1
self.increase_indent()
diff --git a/tools/hrw4u/src/symbols.py b/tools/hrw4u/src/symbols.py
index a0f66de5fe..c40010c64d 100644
--- a/tools/hrw4u/src/symbols.py
+++ b/tools/hrw4u/src/symbols.py
@@ -43,7 +43,7 @@ class SymbolResolver(SymbolResolverBase):
return params.target, params.validate
raise SymbolResolutionError(name, "Unknown operator or invalid
standalone use")
- def declare_variable(self, name: str, type_name: str) -> str:
+ def declare_variable(self, name: str, type_name: str, explicit_slot: int |
None = None) -> str:
try:
var_type = types.VarType.from_str(type_name)
except ValueError as e:
@@ -51,13 +51,24 @@ class SymbolResolver(SymbolResolverBase):
error.add_note(f"Available types: {', '.join([vt.name for vt in
types.VarType])}")
raise error
- if self._var_counter[var_type] >= var_type.limit:
- error = SymbolResolutionError(name, f"Too many '{type_name}'
variables (max {var_type.limit})")
- error.add_note(f"Current count: {self._var_counter[var_type]}")
- raise error
+ # Determine slot number
+ if explicit_slot is not None:
+ if explicit_slot < 0 or explicit_slot >= var_type.limit:
+ raise SymbolResolutionError(
+ name, f"Slot @{explicit_slot} out of range for type
'{type_name}' (valid: 0-{var_type.limit-1})")
+ for var_name, sym in self._symbols.items():
+ if sym.var_type == var_type and sym.slot == explicit_slot:
+ raise SymbolResolutionError(name, f"Slot @{explicit_slot}
already used by variable '{var_name}'")
+
+ slot = explicit_slot
+ else:
+ used_slots = {sym.slot for sym in self._symbols.values() if
sym.var_type == var_type}
+ slot = next((i for i in range(var_type.limit) if i not in
used_slots), None)
+
+ if slot is None:
+ raise SymbolResolutionError(name, f"No available slots for
type '{type_name}' (max {var_type.limit})")
- symbol = types.Symbol(var_type, self._var_counter[var_type])
- self._var_counter[var_type] += 1
+ symbol = types.Symbol(var_type, slot)
self._symbols[name] = symbol
return symbol.as_cond()
diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py
index 4185f78840..0cacafe673 100644
--- a/tools/hrw4u/src/types.py
+++ b/tools/hrw4u/src/types.py
@@ -163,13 +163,13 @@ class VarType(Enum):
@dataclass(slots=True, frozen=True)
class Symbol:
var_type: VarType
- index: int
+ slot: int
def as_cond(self) -> str:
- return f"%{{STATE-{self.var_type.cond_tag}:{self.index}}}"
+ return f"%{{STATE-{self.var_type.cond_tag}:{self.slot}}}"
def as_operator(self, value: str) -> str:
- return f"{self.var_type.op_tag} {self.index} {value}"
+ return f"{self.var_type.op_tag} {self.slot} {value}"
class MapParams:
diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py
index c038cdc241..a3cbc285da 100644
--- a/tools/hrw4u/src/visitor.py
+++ b/tools/hrw4u/src/visitor.py
@@ -399,17 +399,21 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
if ctx.typeName is None:
raise SymbolResolutionError("variable", "Missing type name
in declaration")
name = ctx.name.text
- type = ctx.typeName.text
+ type_name = ctx.typeName.text
+ explicit_slot = int(ctx.slot.text) if ctx.slot else None
if '.' in name or ':' in name:
raise SymbolResolutionError("variable", f"Variable name
'{name}' cannot contain '.' or ':' characters")
- symbol = self.symbol_resolver.declare_variable(name, type)
- self._dbg(f"bind `{name}' to {symbol}")
+ symbol = self.symbol_resolver.declare_variable(name,
type_name, explicit_slot)
+ slot_info = f" @{explicit_slot}" if explicit_slot is not None
else ""
+ self._dbg(f"bind `{name}' to {symbol}{slot_info}")
except Exception as e:
name = getattr(ctx, 'name', None)
type_name = getattr(ctx, 'typeName', None)
- note = f"Variable declaration: {name.text}:{type_name.text}"
if name and type_name else None
+ slot = getattr(ctx, 'slot', None)
+ note = f"Variable declaration: {name.text}:{type_name.text}" +
\
+ (f" @{slot.text}" if slot else "") if name and type_name
else None
with self.trap(ctx, note=note):
raise e
return
@@ -445,6 +449,15 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
for item in ctx.blockItem():
if item.statement():
self.visit(item.statement())
+ elif item.conditional():
+ # Nested conditional - emit if/endif operators with
saved state
+ self.emit_statement("if")
+ saved_indents = self.stmt_indent, self.cond_indent
+ self.stmt_indent += 1
+ self.cond_indent = self.stmt_indent
+ self.visit(item.conditional())
+ self.stmt_indent, self.cond_indent = saved_indents
+ self.emit_statement("endif")
elif item.commentLine() and self.preserve_comments:
self.visit(item.commentLine())
@@ -464,7 +477,7 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
else:
lhs = self.visitFunctionCall(comp.functionCall())
if not lhs:
- return # Skip on error
+ return
operator = ctx.getChild(1)
negate = operator.symbol.type in (hrw4uParser.NEQ,
hrw4uParser.NOT_TILDE)
@@ -496,7 +509,6 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
case _ if ctx.set_():
inner = ctx.set_().getText()[1:-1]
# We no longer strip the quotes here for sets, fixed in
#12256
- # parts = [s.strip().strip("'") for s in inner.split(",")]
cond_txt = f"{lhs} ({inner})"
case _:
diff --git a/tools/hrw4u/tests/data/conds/nested-ifs.ast.txt
b/tools/hrw4u/tests/data/conds/nested-ifs.ast.txt
new file mode 100644
index 0000000000..0a10091aeb
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nested-ifs.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (varSection VARS { (variables (variablesItem
(variableDecl bool_0 : bool ;)) (variablesItem (variableDecl bool_1 : bool ;))
(variablesItem (variableDecl bool_2 : bool ;))) }))) (programItem (section
REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term
(factor (comparison (comparable inbound.req.X-Foo) == (value "bar")))))) (block
{ (blockItem (statement inbound.req.X-Hello = (value "there") ;)) (blockItem
(conditional (ifStatement [...]
diff --git a/tools/hrw4u/tests/data/conds/nested-ifs.input.txt
b/tools/hrw4u/tests/data/conds/nested-ifs.input.txt
new file mode 100644
index 0000000000..2716478531
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nested-ifs.input.txt
@@ -0,0 +1,27 @@
+VARS {
+ bool_0: bool;
+ bool_1: bool;
+ bool_2: bool;
+}
+
+REMAP {
+ if inbound.req.X-Foo == "bar" {
+ inbound.req.X-Hello = "there";
+ if inbound.req.X-Fie == "fie" {
+ inbound.req.X-first = "1";
+ if bool_0 || (bool_1 && bool_2) {
+ inbound.req.X-Parsed = "more";
+ } else {
+ inbound.req.X-Parsed = "yes";
+ }
+ } elif inbound.req.X-Fum == "bar" {
+ inbound.req.X-Parsed = "no";
+ } else {
+ inbound.req.X-More = "yes";
+ }
+ } elif inbound.req.X-Foo == "foo" with NOCASE,PRE {
+ inbound.req.X-Nocase = "foo";
+ } else {
+ inbound.req.X-Something = "no-bar";
+ }
+}
diff --git a/tools/hrw4u/tests/data/conds/nested-ifs.output.txt
b/tools/hrw4u/tests/data/conds/nested-ifs.output.txt
new file mode 100644
index 0000000000..f7b802f5af
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nested-ifs.output.txt
@@ -0,0 +1,27 @@
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{CLIENT-HEADER:X-Foo} ="bar"
+ set-header X-Hello "there"
+ if
+ cond %{CLIENT-HEADER:X-Fie} ="fie"
+ set-header X-first "1"
+ if
+ cond %{STATE-FLAG:0} [OR]
+ cond %{GROUP}
+ cond %{STATE-FLAG:1} [AND]
+ cond %{STATE-FLAG:2}
+ cond %{GROUP:END}
+ set-header X-Parsed "more"
+ else
+ set-header X-Parsed "yes"
+ endif
+ elif
+ cond %{CLIENT-HEADER:X-Fum} ="bar"
+ set-header X-Parsed "no"
+ else
+ set-header X-More "yes"
+ endif
+elif
+ cond %{CLIENT-HEADER:X-Foo} ="foo" [NOCASE,PRE]
+ set-header X-Nocase "foo"
+else
+ set-header X-Something "no-bar"
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt
b/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt
index c05550731a..8bc6dbdf7d 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt
@@ -1 +1,3 @@
-Invalid boolean value 'invalid_value'. Must be one of: 0, 1, FALSE, NO, OFF,
ON, TRUE, YES
\ No newline at end of file
+tests/data/ops/http_cntl_invalid_bool.fail.input.txt:2:4: error: Invalid
boolean value 'invalid_value'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE,
YES
+ 2 | http.cntl.LOGGING = invalid_value;
+ | ^
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt
b/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt
index 4a4000facc..26e842fd6a 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt
@@ -1 +1,3 @@
-Invalid boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON,
TRUE, YES and must not be quoted
\ No newline at end of file
+tests/data/ops/http_cntl_quoted_bool.fail.input.txt:2:4: error: Invalid
boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES and
must not be quoted
+ 2 | http.cntl.LOGGING = "true";
+ | ^
diff --git a/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt
b/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt
index 4a4000facc..8ba0760bb4 100644
--- a/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt
+++ b/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt
@@ -1 +1,3 @@
-Invalid boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON,
TRUE, YES and must not be quoted
\ No newline at end of file
+tests/data/ops/skip_remap_quoted_bool.fail.input.txt:2:4: error: Invalid
boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES and
must not be quoted
+ 2 | skip-remap("true");
+ | ^
diff --git a/tools/hrw4u/tests/data/vars/exceptions.txt
b/tools/hrw4u/tests/data/vars/exceptions.txt
new file mode 100644
index 0000000000..64e57bf9bb
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/exceptions.txt
@@ -0,0 +1,5 @@
+# Operations tests direction exceptions
+# Format: test_name: direction
+#
+# Explicit slot assignment syntax cannot be reversed
+explicit_slots.input: hrw4u
diff --git a/tools/hrw4u/tests/data/vars/explicit_slots.ast.txt
b/tools/hrw4u/tests/data/vars/explicit_slots.ast.txt
new file mode 100644
index 0000000000..1d0b442dae
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/explicit_slots.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (varSection VARS { (variables (variablesItem
(variableDecl parent_config : bool @ 7 ;)) (variablesItem (variableDecl
parent_child : bool @ 12 ;)) (variablesItem (variableDecl match : bool ;))
(variablesItem (variableDecl active_flag : bool @ 3 ;)) (variablesItem
(variableDecl counter : int8 @ 2 ;)) (variablesItem (variableDecl priority :
int8 ;)) (variablesItem (variableDecl status : int16 ;))) }))) (programItem
(section SEND_RESPONSE { (sectionBody (condit [...]
diff --git a/tools/hrw4u/tests/data/vars/explicit_slots.input.txt
b/tools/hrw4u/tests/data/vars/explicit_slots.input.txt
new file mode 100644
index 0000000000..e8ff9f5a6a
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/explicit_slots.input.txt
@@ -0,0 +1,15 @@
+VARS {
+ parent_config: bool @7;
+ parent_child: bool @12;
+ match: bool;
+ active_flag: bool @3;
+ counter: int8 @2;
+ priority: int8;
+ status: int16;
+}
+
+SEND_RESPONSE {
+ if parent_config {
+ inbound.resp.X-Parent = true;
+ }
+}
diff --git a/tools/hrw4u/tests/data/vars/explicit_slots.output.txt
b/tools/hrw4u/tests/data/vars/explicit_slots.output.txt
new file mode 100644
index 0000000000..adaa5f08a5
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/explicit_slots.output.txt
@@ -0,0 +1,3 @@
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+cond %{STATE-FLAG:7}
+ set-header X-Parent true
diff --git a/tools/hrw4u/tests/data/vars/slot_conflict.fail.error.txt
b/tools/hrw4u/tests/data/vars/slot_conflict.fail.error.txt
new file mode 100644
index 0000000000..721dbcddf0
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/slot_conflict.fail.error.txt
@@ -0,0 +1,3 @@
+tests/data/vars/slot_conflict.fail.input.txt:3:4: error: Slot @5 already used
by variable 'first'
+ 3 | second: bool @5; # Error: slot already used
+ | ^
diff --git a/tools/hrw4u/tests/data/vars/slot_conflict.fail.input.txt
b/tools/hrw4u/tests/data/vars/slot_conflict.fail.input.txt
new file mode 100644
index 0000000000..f60cabb213
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/slot_conflict.fail.input.txt
@@ -0,0 +1,8 @@
+VARS {
+ first: bool @5;
+ second: bool @5; # Error: slot already used
+}
+
+SEND_RESPONSE {
+ set_header("X-Test", "value");
+}
diff --git a/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt
b/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt
index 5846224ebd..513c141859 100644
--- a/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt
+++ b/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt
@@ -1,3 +1,3 @@
-tests/data/vars/vars_count.fail.input.txt:7:3: error: Too many 'int8'
variables (max 4)
+tests/data/vars/vars_count.fail.input.txt:7:3: error: No available slots for
type 'int8' (max 4)
7 | Five: int8;
| ^