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
diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst
index 39002b9..11bb275 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -323,9 +323,29 @@
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 e7f8c74..5ec2e89 100644
--- a/doc/admin-guide/plugins/header_rewrite.en.rst
+++ b/doc/admin-guide/plugins/header_rewrite.en.rst
@@ -153,9 +153,19 @@
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 @@
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 @@
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 @@
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 9e0499f..9f5f860 100644
--- a/plugins/header_rewrite/factory.cc
+++ b/plugins/header_rewrite/factory.cc
@@ -92,6 +92,7 @@
} 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 89fe252..6189512 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 @@
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 @@
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 @@
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 @@
// 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 @@
}
// 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 @@
// 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 @@
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 @@
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 @@
// 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 @@
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 2d54f6f..bc23c93 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 @@
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 ac7b750..c8c4f66 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 @@
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 a0b825d..179bd79 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 @@
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 a0f93af..12f0c15 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 @@
} 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 9844ad0..634df46 100644
--- a/plugins/header_rewrite/parser.h
+++ b/plugins/header_rewrite/parser.h
@@ -113,7 +113,7 @@
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 @@
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 50d16ac..28fda37 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"
-///////////////////////////////////////////////////////////////////////////////
-// Class implementation (no reason to have these inline)
-//
+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);
+}
+
void
RuleSet::append(std::unique_ptr<RuleSet> rule)
{
@@ -47,7 +61,7 @@
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 @@
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 @@
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 @@
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 4a5887a..78680bc 100644
--- a/plugins/header_rewrite/ruleset.h
+++ b/plugins/header_rewrite/ruleset.h
@@ -30,74 +30,63 @@
#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);
+
+ ConditionGroup *
+ get_group()
+ {
+ return _op_if.get_group();
+ }
+
+ Parser::CondClause
+ get_clause() const
+ {
+ return _op_if.get_clause();
+ }
+
+ ConditionGroup *
+ new_section(Parser::CondClause clause)
+ {
+ return _op_if.new_section(clause);
+ }
bool
has_operator() const
{
- const CondOpSection *section = &_sections;
+ return _op_if.has_operator();
+ }
- while (section != nullptr) {
- if (section->has_operator()) {
- return true;
- }
- section = section->next;
- }
- return false;
+ bool
+ section_has_condition() const
+ {
+ auto *sec = _op_if.cur_section();
+ return sec ? sec->group.has_conditions() : false;
+ }
+
+ bool
+ section_has_operator() const
+ {
+ auto *sec = _op_if.cur_section();
+ return sec ? sec->has_operator() : false;
}
void
@@ -106,41 +95,12 @@
_hook = hook;
}
- ConditionGroup *
- get_group()
- {
- return &_cur_section->group;
- }
-
TSHttpHookID
get_hook() const
{
return _hook;
}
- Parser::CondClause
- get_clause() const
- {
- return _clause;
- }
-
- CondOpSection *
- cur_section()
- {
- return _cur_section;
- }
-
- ConditionGroup *
- new_section(Parser::CondClause clause)
- {
- TSAssert(_cur_section && !_cur_section->next);
- _clause = clause;
- _cur_section->next = new CondOpSection();
- _cur_section = _cur_section->next;
-
- return &_cur_section->group;
- }
-
ResourceIDs
get_resource_ids() const
{
@@ -153,49 +113,14 @@
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 0000000..aaa750a
--- /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 0000000..a04b910
--- /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 0000000..09d4e47
--- /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 0000000..b583ca3
--- /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 0000000..126f4bd
--- /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 0000000..f975acf
--- /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 ed26875..e7ef3b6 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 @@
"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 @@
"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 @@
"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 0f909f1..c957bfa 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 @@
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/nested_ifs.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf
new file mode 100644
index 0000000..5453b0b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf
@@ -0,0 +1,44 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# 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 %{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 a91e314..5db49f5 100644
--- a/tools/hrw4u/grammar/hrw4u.g4
+++ b/tools/hrw4u/grammar/hrw4u.g4
@@ -80,6 +80,7 @@
COLON : ':';
COMMA : ',';
SEMICOLON : ';';
+AT : '@';
COMMENT : '#' ~[\r\n]* ;
WS : [ \t\r\n]+ -> skip ;
@@ -121,7 +122,7 @@
;
variableDecl
- : name=IDENT COLON typeName=IDENT SEMICOLON
+ : name=IDENT COLON typeName=IDENT (AT slot=NUMBER)? SEMICOLON
;
statement
@@ -156,6 +157,7 @@
blockItem
: statement
+ | conditional
| commentLine
;
diff --git a/tools/hrw4u/grammar/u4wrh.g4 b/tools/hrw4u/grammar/u4wrh.g4
index a88b2c5..5397288 100644
--- a/tools/hrw4u/grammar/u4wrh.g4
+++ b/tools/hrw4u/grammar/u4wrh.g4
@@ -21,6 +21,8 @@
// Lexer Rules
// -----------------------------
COND : 'cond';
+IF_OP : 'if';
+ENDIF_OP : 'endif';
ELIF : 'elif';
ELSE : 'else';
AND_MOD : 'AND';
@@ -48,24 +50,24 @@
// Percent blocks - treat entire %{...} as one token
PERCENT_BLOCK : '%{' ~[}\r\n]* '}' '}'?;
-IDENT : [@a-zA-Z_][a-zA-Z0-9_@.-]* ;
-COMPLEX_STRING : (~[ \t\r\n[\]{}(),=!><~%])+;
-NUMBER : [0-9]+ ;
-LPAREN : '(';
-RPAREN : ')';
-LBRACE : '{';
-RBRACE : '}';
-LBRACKET : '[';
-RBRACKET : ']';
-EQUALS : '=';
-NEQ : '!=';
-GT : '>';
-LT : '<';
-COMMA : ',';
+IDENT : [@a-zA-Z_][a-zA-Z0-9_@.-]* ;
+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 ;
+EOL : '\r'? '\n';
+COMMENT : '#'~[\r\n]*;
+WS : [ \t]+ -> skip ;
// -----------------------------
// Parser Rules
@@ -78,6 +80,8 @@
line
: condLine EOL
| opLine EOL
+ | ifLine EOL
+ | endifLine EOL
| elifLine EOL
| elseLine EOL
| commentLine EOL
@@ -88,6 +92,14 @@
: COND condBody modList?
;
+ifLine
+ : IF_OP
+ ;
+
+endifLine
+ : ENDIF_OP
+ ;
+
elifLine
: ELIF
;
diff --git a/tools/hrw4u/pyproject.toml b/tools/hrw4u/pyproject.toml
index 5a85df7..f66a9ad 100644
--- a/tools/hrw4u/pyproject.toml
+++ b/tools/hrw4u/pyproject.toml
@@ -20,7 +20,7 @@
[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 = "leif@apache.org"}
diff --git a/tools/hrw4u/scripts/testcase.py b/tools/hrw4u/scripts/testcase.py
index 757af95..802e32f 100755
--- a/tools/hrw4u/scripts/testcase.py
+++ b/tools/hrw4u/scripts/testcase.py
@@ -29,6 +29,29 @@
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 @@
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 @@
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)
- print(f"\nUpdated: {total - failed}, Failed: {failed}")
+ 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 - 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 d08c343..149b139 100644
--- a/tools/hrw4u/src/hrw_visitor.py
+++ b/tools/hrw4u/src/hrw_visitor.py
@@ -54,8 +54,9 @@
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 @@
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 @@
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 @@
# 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 @@
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 @@
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 @@
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 a0f66de..c40010c 100644
--- a/tools/hrw4u/src/symbols.py
+++ b/tools/hrw4u/src/symbols.py
@@ -43,7 +43,7 @@
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 @@
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}'")
- symbol = types.Symbol(var_type, self._var_counter[var_type])
- self._var_counter[var_type] += 1
+ 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, slot)
self._symbols[name] = symbol
return symbol.as_cond()
diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py
index 4185f78..0cacafe 100644
--- a/tools/hrw4u/src/types.py
+++ b/tools/hrw4u/src/types.py
@@ -163,13 +163,13 @@
@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 c038cdc..a3cbc28 100644
--- a/tools/hrw4u/src/visitor.py
+++ b/tools/hrw4u/src/visitor.py
@@ -399,17 +399,21 @@
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 @@
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 @@
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 @@
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 0000000..0a10091
--- /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 if (condition (expression (term (factor (comparison (comparable inbound.req.X-Fie) == (value "fie")))))) (block { (blockItem (statement inbound.req.X-first = (value "1") ;)) (blockItem (conditional (ifStatement if (condition (expression (expression (term (factor bool_0))) || (term (factor ( (expression (term (term (factor bool_1)) && (factor bool_2))) ))))) (block { (blockItem (statement inbound.req.X-Parsed = (value "more") ;)) })) (elseClause else (block { (blockItem (statement inbound.req.X-Parsed = (value "yes") ;)) })))) })) (elifClause elif (condition (expression (term (factor (comparison (comparable inbound.req.X-Fum) == (value "bar")))))) (block { (blockItem (statement inbound.req.X-Parsed = (value "no") ;)) })) (elseClause else (block { (blockItem (statement inbound.req.X-More = (value "yes") ;)) })))) })) (elifClause elif (condition (expression (term (factor (comparison (comparable inbound.req.X-Foo) == (value "foo") (modifier with (modifierList NOCASE , PRE))))))) (block { (blockItem (statement inbound.req.X-Nocase = (value "foo") ;)) })) (elseClause else (block { (blockItem (statement inbound.req.X-Something = (value "no-bar") ;)) })))) })) <EOF>)
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 0000000..2716478
--- /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 0000000..f7b802f
--- /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 c055507..8bc6dbd 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 4a4000f..26e842f 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 4a4000f..8ba0760 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 0000000..64e57bf
--- /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 0000000..1d0b442
--- /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 (conditional (ifStatement if (condition (expression (term (factor parent_config)))) (block { (blockItem (statement inbound.resp.X-Parent = (value true) ;)) })))) })) <EOF>)
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 0000000..e8ff9f5
--- /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 0000000..adaa5f0
--- /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 0000000..721dbcd
--- /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 0000000..f60cabb
--- /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 5846224..513c141 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;
| ^