Make var name collapse underscores
This commit is contained in:
parent
5ae85ad11e
commit
3573e8e750
2 changed files with 57 additions and 2 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
|
@ -57,8 +58,20 @@ class BaseHandler:
|
||||||
role_prefix_section_subsection_key
|
role_prefix_section_subsection_key
|
||||||
|
|
||||||
Sanitises parts to lowercase [a-z0-9_] and strips extras.
|
Sanitises parts to lowercase [a-z0-9_] and strips extras.
|
||||||
|
|
||||||
|
Consecutive separators are collapsed to a single underscore. This is
|
||||||
|
required for correctness, not just aesthetics: a source key such as
|
||||||
|
``log..level`` or ``cache--size`` would otherwise sanitise to a name
|
||||||
|
containing a double underscore (``log__level``). The output safety gate
|
||||||
|
in ``safety.py`` deliberately rejects *any* ``__`` in a generated
|
||||||
|
identifier because ``__`` is the gateway to every Jinja2 SSTI gadget
|
||||||
|
(``__class__``/``__globals__``/...). Emitting a dunder here would make
|
||||||
|
JinjaTurtle's own gate reject JinjaTurtle's own placeholder, aborting
|
||||||
|
generation on entirely benign config. Collapsing runs keeps every
|
||||||
|
generated name a plain single-underscore-delimited identifier that the
|
||||||
|
gate accepts.
|
||||||
"""
|
"""
|
||||||
role_prefix = role_prefix.strip().lower()
|
role_prefix = re.sub(r"_+", "_", role_prefix.strip().lower())
|
||||||
clean_parts: list[str] = []
|
clean_parts: list[str] = []
|
||||||
|
|
||||||
for part in path:
|
for part in path:
|
||||||
|
|
@ -70,7 +83,9 @@ class BaseHandler:
|
||||||
cleaned_chars.append(c.lower())
|
cleaned_chars.append(c.lower())
|
||||||
else:
|
else:
|
||||||
cleaned_chars.append("_")
|
cleaned_chars.append("_")
|
||||||
cleaned_part = "".join(cleaned_chars).strip("_")
|
# Collapse runs of underscores (from adjacent separators) to a
|
||||||
|
# single "_" so the result can never contain a forbidden "__".
|
||||||
|
cleaned_part = re.sub(r"_+", "_", "".join(cleaned_chars)).strip("_")
|
||||||
if cleaned_part:
|
if cleaned_part:
|
||||||
clean_parts.append(cleaned_part)
|
clean_parts.append(cleaned_part)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,3 +191,43 @@ def test_flatten_config_unsupported_format():
|
||||||
flatten_config("bogusfmt", parsed=None)
|
flatten_config("bogusfmt", parsed=None)
|
||||||
|
|
||||||
assert "Unsupported format" in str(exc.value)
|
assert "Unsupported format" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_var_name_collapses_adjacent_separators():
|
||||||
|
"""Regression: adjacent separators must not produce a forbidden ``__``.
|
||||||
|
|
||||||
|
A source key such as ``log..level`` or ``cache--size`` previously sanitised
|
||||||
|
to a name containing a double underscore (``..._log__level``). The output
|
||||||
|
safety gate in safety.py rejects *any* ``__`` in a generated identifier
|
||||||
|
(it is the gateway to Jinja2 SSTI gadgets), so emitting one made JinjaTurtle
|
||||||
|
reject its own placeholder and abort generation on entirely benign config.
|
||||||
|
make_var_name now collapses runs of underscores to a single ``_``.
|
||||||
|
"""
|
||||||
|
for raw_key in ("log..level", "cache--size", "a...b", "x.-.y", "a..b--c"):
|
||||||
|
name = make_var_name("role", ("main", raw_key))
|
||||||
|
assert "__" not in name, f"{raw_key!r} produced {name!r}"
|
||||||
|
# Still a valid Ansible/Jinja identifier.
|
||||||
|
assert name.replace("_", "").isalnum() or name == "role"
|
||||||
|
|
||||||
|
# The double-underscore-free collapse is stable and predictable.
|
||||||
|
assert make_var_name("role", ("main", "log..level")) == "role_main_log_level"
|
||||||
|
assert make_var_name("role", ("main", "cache--size")) == "role_main_cache_size"
|
||||||
|
# A leading-digit-free prefix with its own repeated separators is collapsed too.
|
||||||
|
assert make_var_name("My__Role", ("k",)) == "my_role_k"
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_var_name_collapsed_names_pass_output_safety_gate():
|
||||||
|
"""The names make_var_name emits must be accepted by the safety gate.
|
||||||
|
|
||||||
|
This binds the two modules together: whatever identifier make_var_name
|
||||||
|
produces for a hostile-looking key must lex as the JinjaTurtle subset, so a
|
||||||
|
real template built from it is not refused.
|
||||||
|
"""
|
||||||
|
from jinjaturtle.safety import verify_jinja2_template_safe
|
||||||
|
|
||||||
|
for raw_key in ("log..level", "cache--size", "a...b", "weird..key..name"):
|
||||||
|
var = make_var_name("demo", ("section", raw_key))
|
||||||
|
# Build the kind of expression a handler would emit for this variable.
|
||||||
|
template = "{{ " + var + " }}"
|
||||||
|
# Must not raise TemplateSafetyError.
|
||||||
|
verify_jinja2_template_safe(template)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue