feat(tpl): make escaping mechanism more foolproof

Treat "${$}" as literal "$". This allows to template express
strings, for example, of the form "${some_var_value}" where
`some_var_value` is interpolated from bindings.
This commit is contained in:
Andrew Mayorov 2023-05-05 10:24:47 +03:00
parent f689d6c233
commit 343b679741
No known key found for this signature in database
GPG Key ID: 2837C62ACFBFED5D
4 changed files with 28 additions and 26 deletions

View File

@ -133,7 +133,7 @@ handle_disallowed_placeholders(Template, Source) ->
allowed => #{placeholders => ?ALLOWED_VARS},
notice =>
"Disallowed placeholders will be rendered as is."
" However, consider using `$${...}` escaping for literal `${...}` where"
" However, consider using `${$}` escaping for literal `$` where"
" needed to avoid unexpected results."
}),
Result = prerender_disallowed_placeholders(Template),
@ -153,7 +153,7 @@ prerender_disallowed_placeholders(Template) ->
% parse as a literal string.
case lists:member(Name, ?ALLOWED_VARS) of
true -> "${" ++ Name ++ "}";
false -> "$${" ++ Name ++ "}"
false -> "${$}{" ++ Name ++ "}"
end
end
}),

View File

@ -136,7 +136,7 @@ handle_disallowed_placeholders(Template, Source, Allowed) ->
allowed => #{placeholders => Allowed},
notice =>
"Disallowed placeholders will be rendered as is."
" However, consider using `$${...}` escaping for literal `${...}` where"
" However, consider using `${$}` escaping for literal `$` where"
" needed to avoid unexpected results."
}),
Result = prerender_disallowed_placeholders(Template, Allowed),
@ -156,7 +156,7 @@ prerender_disallowed_placeholders(Template, Allowed) ->
% parse as a literal string.
case lists:member(Name, Allowed) of
true -> "${" ++ Name ++ "}";
false -> "$${" ++ Name ++ "}"
false -> "${$}{" ++ Name ++ "}"
end
end
}),

View File

@ -42,7 +42,7 @@
-type t() :: str() | {'$tpl', deeptpl()}.
-type str() :: [unicode:chardata() | placeholder()].
-type str() :: [iodata() | byte() | placeholder()].
-type deep() :: {'$tpl', deeptpl()}.
-type deeptpl() ::
@ -76,7 +76,8 @@
var_trans => var_trans()
}.
-define(RE_PLACEHOLDER, "\\$(\\$?)\\{[.]?([a-zA-Z0-9._]*)\\}").
-define(RE_PLACEHOLDER, "\\$\\{[.]?([a-zA-Z0-9._]*)\\}").
-define(RE_ESCAPE, "\\$\\{(\\$)\\}").
%% @doc Parse a unicode string into a template.
%% String might contain zero or more of placeholders in the form of `${var}`,
@ -95,22 +96,21 @@ parse(String, Opts) ->
RE =
case Opts of
#{strip_double_quote := true} ->
<<"((?|" ?RE_PLACEHOLDER "|\"" ?RE_PLACEHOLDER "\"))">>;
<<"((?|" ?RE_PLACEHOLDER "|\"" ?RE_PLACEHOLDER "\")|" ?RE_ESCAPE ")">>;
#{} ->
<<"(" ?RE_PLACEHOLDER ")">>
<<"(" ?RE_PLACEHOLDER "|" ?RE_ESCAPE ")">>
end,
Splits = re:split(String, RE, [{return, binary}, group, trim, unicode]),
Components = lists:flatmap(fun parse_split/1, Splits),
Components.
parse_split([Part, _PH, <<>>, Var]) ->
parse_split([Part, _PH, Var, <<>>]) ->
% Regular placeholder
prepend(Part, [{var, unicode:characters_to_list(Var), parse_accessor(Var)}]);
parse_split([Part, _PH = <<B1, $$, Rest/binary>>, <<"$">>, _]) ->
% Escaped literal, take all but the second byte, which is always `$`.
% Important to make a whole token starting with `$` so the `unparse/11`
% function can distinguish escaped literals.
prepend(Part, [<<B1, Rest/binary>>]);
parse_split([Part, _Escape, <<>>, <<"$">>]) ->
% Escaped literal `$`.
% Use single char as token so the `unparse/1` function can distinguish escaped `$`.
prepend(Part, [$$]);
parse_split([Tail]) ->
[Tail].
@ -159,8 +159,8 @@ unparse(Template) ->
unparse_part({var, Name, _Accessor}) ->
render_placeholder(Name);
unparse_part(Part = <<"${", _/binary>>) ->
<<"$", Part/binary>>;
unparse_part($$) ->
<<"${$}">>;
unparse_part(Part) ->
Part.

View File

@ -115,7 +115,7 @@ t_render_missing_bindings(_) ->
).
t_unparse(_) ->
TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}}">>,
TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>,
Template = emqx_connector_template:parse(TString),
?assertEqual(
TString,
@ -129,12 +129,14 @@ t_const(_) ->
),
?assertEqual(
false,
emqx_connector_template:is_const(emqx_connector_template:parse(<<"a:${a},b:${b},c:$${c}">>))
emqx_connector_template:is_const(
emqx_connector_template:parse(<<"a:${a},b:${b},c:${$}{c}">>)
)
),
?assertEqual(
true,
emqx_connector_template:is_const(
emqx_connector_template:parse(<<"a:$${a},b:$${b},c:$${c}">>)
emqx_connector_template:parse(<<"a:${$}{a},b:${$}{b}">>)
)
).
@ -147,16 +149,16 @@ t_render_partial_ph(_) ->
).
t_parse_escaped(_) ->
Bindings = #{a => <<"1">>, b => 1},
Template = emqx_connector_template:parse(<<"a:${a},b:$${b}">>),
Bindings = #{a => <<"1">>, b => 1, c => "VAR"},
Template = emqx_connector_template:parse(<<"a:${a},b:${$}{b},c:${$}{${c}},lit:${$}{$}">>),
?assertEqual(
<<"a:1,b:${b}">>,
<<"a:1,b:${b},c:${VAR},lit:${$}">>,
render_strict_string(Template, Bindings)
).
t_parse_escaped_dquote(_) ->
Bindings = #{a => <<"1">>, b => 1},
Template = emqx_connector_template:parse(<<"a:\"${a}\",b:\"$${b}\"">>, #{
Template = emqx_connector_template:parse(<<"a:\"${a}\",b:\"${$}{b}\"">>, #{
strip_double_quote => true
}),
?assertEqual(
@ -299,7 +301,7 @@ t_render_tmpl_deep(_) ->
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
Template = emqx_connector_template:parse_deep(
#{<<"${a}">> => [<<"${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>, <<"$${d}">>], 0}]}
#{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>, <<"${$}{d}">>], 0}]}
),
?assertEqual(
@ -308,12 +310,12 @@ t_render_tmpl_deep(_) ->
),
?assertEqual(
#{<<"1">> => [<<"1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
#{<<"1">> => [<<"$1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
emqx_connector_template:render_strict(Template, Bindings)
).
t_unparse_tmpl_deep(_) ->
Term = #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>], 0}]},
Term = #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>], <<"${$}{d}">>, 0}]},
Template = emqx_connector_template:parse_deep(Term),
?assertEqual(Term, emqx_connector_template:unparse(Template)).