diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index d9b20a47c..0a938eafb 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -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 }), diff --git a/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl b/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl index bd7b353a5..444955504 100644 --- a/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl +++ b/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl @@ -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 }), diff --git a/apps/emqx_connector/src/emqx_connector_template.erl b/apps/emqx_connector/src/emqx_connector_template.erl index 72062fc2c..619dbd6ec 100644 --- a/apps/emqx_connector/src/emqx_connector_template.erl +++ b/apps/emqx_connector/src/emqx_connector_template.erl @@ -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 = <>, <<"$">>, _]) -> - % 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, [<>]); +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. diff --git a/apps/emqx_connector/test/emqx_connector_template_SUITE.erl b/apps/emqx_connector/test/emqx_connector_template_SUITE.erl index b6784ea54..3700caa96 100644 --- a/apps/emqx_connector/test/emqx_connector_template_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_template_SUITE.erl @@ -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)).