From e30badcc837f4a1ad62022254a954494c4adb719 Mon Sep 17 00:00:00 2001 From: Andrew Maiorov Date: Fri, 31 Jul 2026 11:34:45 +0200 Subject: [PATCH 1/2] fix: process unicode escape sequences This commit adds missing processing for JSON-compatible Unicode escape sequences, adapted from OTP-28's `json` module. --- src/hocon_scanner.xrl | 47 ++++++++++++++++++++++++++++++++++ test/hocon_tests.erl | 59 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/hocon_scanner.xrl b/src/hocon_scanner.xrl index f52d173..5fb5cc9 100644 --- a/src/hocon_scanner.xrl +++ b/src/hocon_scanner.xrl @@ -232,8 +232,55 @@ unesc([$\\, $t | T]) -> {$\t, T}; unesc([$\\, $r | T]) -> {$\r, T}; unesc([$\\, $b | T]) -> {$\b, T}; unesc([$\\, $f | T]) -> {$\f, T}; +unesc([$\\, $u | T]) -> unescapeu(T); unesc([H | T]) -> {H, T}. +-define(unescapeu_invalid(ESC), ?unescapeu_invalid(invalid_uescape, ESC)). +-define(unescapeu_invalid(R, ESC), + throw({scan_error, #{reason => R, escape => ESC}}) +). + +unescapeu([E1, E2, E3, E4 | T]) -> + try hex_to_int(E1, E2, E3, E4) of + CP when CP >= 16#D800, CP =< 16#DBFF -> + unescape_surrogate(T, CP); + CP -> + try <> of + _ -> {CP, T} + catch + _:_ -> ?unescapeu_invalid([$u, E1, E2, E3, E4]) + end + catch + _:_ -> + ?unescapeu_invalid([$u, E1, E2, E3, E4]) + end; +unescapeu(Esc) -> + ?unescapeu_invalid([$u | Esc]). + +unescape_surrogate([$\\, $u, E1, E2, E3, E4 | T], Hi) -> + try hex_to_int(E1, E2, E3, E4) of + Lo when Lo >= 16#DC00, Lo =< 16#DFFF -> + CP = 16#10000 + ((Hi band 16#3FF) bsl 10) + (Lo band 16#3FF), + try <> of + _ -> {CP, T} + catch + _:_ -> ?unescapeu_invalid(invalid_uescape_surrogate, [$u, E1, E2, E3, E4]) + end; + _ -> + ?unescapeu_invalid(invalid_uescape_surrogate, [$u, E1, E2, E3, E4]) + catch + _:_ -> ?unescapeu_invalid([$u, E1, E2, E3, E4]) + end; +unescape_surrogate(Esc, _Hi) -> + ?unescapeu_invalid([$u | Esc]). + +hex_to_int(H1, H2, H3, H4) -> + hex_digit(H4) + 16 * (hex_digit(H3) + 16 * (hex_digit(H2) + 16 * hex_digit(H1))). + +hex_digit(C) when C >= $a, C =< $f -> C - $a + 10; +hex_digit(C) when C >= $A, C =< $F -> C - $A + 10; +hex_digit(C) when C >= $0, C =< $9 -> C - $0. + maybe_var_ref_name("${?" ++ Name_CR) -> [$} | NameRev] = lists:reverse(Name_CR), unicode:characters_to_binary(string:trim(lists:reverse(NameRev)), utf8). diff --git a/test/hocon_tests.erl b/test/hocon_tests.erl index 847a492..e5f73da 100644 --- a/test/hocon_tests.erl +++ b/test/hocon_tests.erl @@ -272,6 +272,65 @@ escape_test_() -> ) ]. +unicode_escape_test_() -> + [ + {"ASCII code point", + ?_assertEqual( + #{<<"k">> => <<"A">>}, + binary(<<"k = \"\\u0041\"">>) + )}, + {"mixed-case hexadecimal BMP code points", + ?_assertEqual( + #{<<"k">> => <<"å你"/utf8>>}, + binary(<<"k = \"\\u00e5\\u4F60\"">>) + )}, + {"unicode escape in a quoted key", + ?_assertEqual( + #{<<"key">> => 1}, + binary(<<"\"\\u006bey\" = 1">>) + )}, + {"surrogate pair", + ?_assertEqual( + #{<<"k">> => <<"😀"/utf8>>}, + binary(<<"k = \"\\uD83D\\uDE00\"">>) + )}, + {"lowest surrogate pair", + ?_assertEqual( + #{<<"k">> => <<16#10000/utf8>>}, + binary(<<"k = \"\\uD800\\uDC00\"">>) + )}, + {"highest surrogate pair", + ?_assertEqual( + #{<<"k">> => <<16#10FFFF/utf8>>}, + binary(<<"k = \"\\uDBFF\\uDFFF\"">>) + )}, + {"lone low surrogate", + ?_assertMatch( + {error, {scan_error, #{reason := invalid_uescape}}}, + hocon:binary(<<"k = \"\\uDC00\"">>) + )}, + {"lone high surrogate", + ?_assertMatch( + {error, {scan_error, #{reason := invalid_uescape}}}, + hocon:binary(<<"k = \"\\uD800\"">>) + )}, + {"high surrogate followed by a BMP code point", + ?_assertMatch( + {error, {scan_error, #{reason := invalid_uescape_surrogate}}}, + hocon:binary(<<"k = \"\\uD800\\u0041\"">>) + )}, + {"malformed unicode escape", + ?_assertMatch( + {error, {scan_error, _}}, + hocon:binary(<<"k = \"\\u123x\"">>) + )}, + {"short unicode escape", + ?_assertMatch( + {error, {scan_error, _}}, + hocon:binary(<<"k = \"\\u123\"">>) + )} + ]. + triple_quote_string_test_() -> Parse = fun(Str) -> maps:get(<<"a">>, binary(<<"a = \"\"\"", Str/binary, "\"\"\"">>)) end, [ From 77a87008ff30d9df2a7b4badfbb2955ecf5ccdd7 Mon Sep 17 00:00:00 2001 From: Andrew Maiorov Date: Fri, 31 Jul 2026 15:53:58 +0200 Subject: [PATCH 2/2] test: verify triple-quoted strings do no unescaping --- test/hocon_tests.erl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/hocon_tests.erl b/test/hocon_tests.erl index e5f73da..46fd324 100644 --- a/test/hocon_tests.erl +++ b/test/hocon_tests.erl @@ -328,6 +328,20 @@ unicode_escape_test_() -> ?_assertMatch( {error, {scan_error, _}}, hocon:binary(<<"k = \"\\u123\"">>) + )}, + {"triple-quoted strings preserve quoting verbatim", + ?_assertEqual( + #{<<"k">> => <<"\\u00e5\\u4F60\\t">>}, + binary(<<"k = \"\"\"\\u00e5\\u4F60\\t\"\"\"">>) + )}, + {"triple-quoted indented strings preserve quoting verbatim", + ?_assertEqual( + #{<<"k">> => <<"\\u00e5\\u4F60\\t\n">>}, + binary(<< + "k = \"\"\"~\n" + " \\u00e5\\u4F60\\t\n" + "~\"\"\"" + >>) )} ].