diff --git a/CHANGELOG.md b/CHANGELOG.md index aaab63f1e1..61fa856e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.7.0-alpha.1] - Unreleased ### Added +- Added `json` module to estdlib, compatible with Erlang/OTP `json` API +- Added `Keyword.put_new/3` to exavmlib +- Added `JSON` module to exavmlib (Elixir wrapper for estdlib `json`) - Added `erlang:node/1` BIF - Added `erts_internal:cmp_term/2` - Added `short` option to `erlang:float_to_binary/2` and `erlang:float_to_list/2` @@ -32,6 +35,9 @@ strict format validation - Fixed `erlang:binary_to_float/1` and `erlang:list_to_float/1` returning `inf` for overflow instead of raising `badarg` +### Removed +- Removed old `json_encoder` module (now standard Erlang/OTP `json` module is available) + ## [0.7.0-alpha.0] - 2026-03-20 ### Added diff --git a/CMakeModules/BuildElixir.cmake b/CMakeModules/BuildElixir.cmake index 9aa0f9df90..c7db3abc22 100644 --- a/CMakeModules/BuildElixir.cmake +++ b/CMakeModules/BuildElixir.cmake @@ -18,10 +18,75 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +# Compile a single .ex source that defines multiple modules. +# All module names must be listed, and OUTPUT names the variable +# that receives the beam paths (for passing to pack_archive). +# +# Usage: +# compile_multi(json.ex +# JSON +# JSON.Encoder +# JSON.Encoder.Atom +# OUTPUT JSON_BEAMS +# ) +# pack_archive(mylib ${MODULES} EXTRA_BEAMS ${JSON_BEAMS}) +# +macro(compile_multi source_file) + find_package(Elixir REQUIRED) + set(_cm_modules "") + set(_cm_outputs "") + set(_cm_outvar "") + set(_cm_in_output FALSE) + foreach(_arg ${ARGN}) + if(_arg STREQUAL "OUTPUT") + set(_cm_in_output TRUE) + elseif(_cm_in_output) + set(_cm_outvar "${_arg}") + else() + list(APPEND _cm_modules "${_arg}") + list(APPEND _cm_outputs + "${CMAKE_CURRENT_BINARY_DIR}/beams/Elixir.${_arg}.beam" + ) + endif() + endforeach() + add_custom_command( + OUTPUT ${_cm_outputs} + COMMAND mkdir -p + ${CMAKE_CURRENT_BINARY_DIR}/beams + ${CMAKE_CURRENT_BINARY_DIR}/beams.tmp.multi.${_cm_outvar} + COMMAND elixirc --no-docs --no-debug-info + --ignore-module-conflict + -o ${CMAKE_CURRENT_BINARY_DIR}/beams.tmp.multi.${_cm_outvar}/ + ${CMAKE_CURRENT_SOURCE_DIR}/${source_file} + COMMAND sh -c + "mv ${CMAKE_CURRENT_BINARY_DIR}/beams.tmp.multi.${_cm_outvar}/Elixir.*.beam ${CMAKE_CURRENT_BINARY_DIR}/beams/" + COMMAND rmdir + ${CMAKE_CURRENT_BINARY_DIR}/beams.tmp.multi.${_cm_outvar} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${source_file} + COMMENT "Compiling ${source_file}" + VERBATIM + ) + set(${_cm_outvar} ${_cm_outputs}) +endmacro() + macro(pack_archive avm_name) find_package(Elixir REQUIRED) - foreach(module_name ${ARGN}) + # Parse EXTRA_BEAMS keyword from arguments + set(_pa_modules "") + set(_pa_extra "") + set(_pa_in_extra FALSE) + foreach(_arg ${ARGN}) + if(_arg STREQUAL "EXTRA_BEAMS") + set(_pa_in_extra TRUE) + elseif(_pa_in_extra) + list(APPEND _pa_extra "${_arg}") + else() + list(APPEND _pa_modules "${_arg}") + endif() + endforeach() + + foreach(module_name ${_pa_modules}) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/beams/Elixir.${module_name}.beam COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/beams ${CMAKE_CURRENT_BINARY_DIR}/beams.tmp.${module_name} @@ -35,6 +100,8 @@ macro(pack_archive avm_name) set(BEAMS ${BEAMS} ${CMAKE_CURRENT_BINARY_DIR}/beams/Elixir.${module_name}.beam) endforeach() + set(BEAMS ${BEAMS} ${_pa_extra}) + add_custom_target( ${avm_name}_beams ALL DEPENDS ${BEAMS} diff --git a/UPDATING.md b/UPDATING.md index 79699394cd..7937cf2285 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -30,6 +30,8 @@ rather than copy sdkconfig.release-defaults.in to sdkconfig.defaults.in (which s `reconfigure` or `set-target` to be run to pick up the changes), this may also be combined with the `ATOMVM_ELIXIR_SUPPORT` option. For example, an Elixir-supported release build is configured using: `idf.py -DATOMVM_ELIXIR_SUPPORT=on -DATOMVM_RELEASE=on set-target ${CHIP}` +- `json_encoder` module has been removed, use new (and standard) `json` module. New module uses +standard Erlang/OTP API, that takes maps instead of proplists. ## v0.6.4 -> v0.6.5 diff --git a/examples/erlang/system_info_server.erl b/examples/erlang/system_info_server.erl index 11ef23d157..3d1b5b1574 100644 --- a/examples/erlang/system_info_server.erl +++ b/examples/erlang/system_info_server.erl @@ -1,7 +1,7 @@ % % This file is part of AtomVM. % -% Copyright 2019-2020 Davide Bettio +% Copyright 2019-2026 Davide Bettio % % Licensed under the Apache License, Version 2.0 (the "License"); % you may not use this file except in compliance with the License. @@ -62,18 +62,18 @@ handle_req("GET", [], Conn) -> Body = [<<"

">>, TimeString, <<"

">>], http_server:reply(200, Body, Conn); handle_req("GET", ["system", "info"], Conn) -> - SysInfo = [ - {atom_count, erlang:system_info(atom_count)}, - {process_count, erlang:system_info(process_count)}, - {port_count, erlang:system_info(port_count)}, - {word_size, erlang:system_info(wordsize)}, - {system_architecture, erlang:system_info(system_architecture)} - ], - Body = json_encoder:encode(SysInfo), + SysInfo = #{ + atom_count => erlang:system_info(atom_count), + process_count => erlang:system_info(process_count), + port_count => erlang:system_info(port_count), + word_size => erlang:system_info(wordsize), + system_architecture => erlang:system_info(system_architecture) + }, + Body = json:encode(SysInfo), http_server:reply(200, Body, Conn); handle_req("GET", ["processes", PidString, "info"], Conn) -> {Code, ProcInfo} = try_proc_info_list(PidString), - Body = json_encoder:encode(ProcInfo), + Body = json:encode(ProcInfo), http_server:reply(Code, Body, Conn); handle_req(Method, Path, Conn) -> io:format("Method: ~p Path: ~p~n", [Method, Path]), diff --git a/libs/eavmlib/src/CMakeLists.txt b/libs/eavmlib/src/CMakeLists.txt index 548511680c..f544f3e787 100644 --- a/libs/eavmlib/src/CMakeLists.txt +++ b/libs/eavmlib/src/CMakeLists.txt @@ -28,7 +28,6 @@ set(ERLANG_MODULES console gpio_hal i2c_hal - json_encoder logger_manager port spi_hal diff --git a/libs/eavmlib/src/json_encoder.erl b/libs/eavmlib/src/json_encoder.erl deleted file mode 100644 index 551d0919e8..0000000000 --- a/libs/eavmlib/src/json_encoder.erl +++ /dev/null @@ -1,78 +0,0 @@ -% -% This file is part of AtomVM. -% -% Copyright 2018-2022 Davide Bettio -% -% Licensed 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. -% -% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later -% - -%%----------------------------------------------------------------------------- -%% @doc JSON specific APIs -%% -%% This module contains functions for working with json data. -%% @end -%%----------------------------------------------------------------------------- --module(json_encoder). --export([encode/1]). - -%%----------------------------------------------------------------------------- -%% @param Data data to encode to json -%% @returns JSON encoded data -%% @doc Convert data to json encoded binary -%% @end -%%----------------------------------------------------------------------------- --spec encode(Data :: any()) -> binary(). -encode(false) -> - <<"false">>; -encode(true) -> - <<"true">>; -encode(nil) -> - <<"nil">>; -encode(null) -> - <<"null">>; -encode(undefined) -> - <<"null">>; -encode(Value) when is_atom(Value) -> - [$", erlang:atom_to_binary(Value, latin1), $"]; -encode(Value) when is_binary(Value) -> - [$", Value, $"]; -encode(Value) when is_float(Value) -> - erlang:float_to_binary(Value, [{decimals, 32}, compact]); -encode(Value) when is_integer(Value) -> - erlang:integer_to_binary(Value); -encode(V) -> - encode(V, []). - -encode([{_K, _V} | _T] = L, []) -> - encode(L, ${); -encode([{Key, Value} | []], Acc) -> - Encoded = [$", encode_key(Key), "\":", encode(Value), $}], - [Acc | Encoded]; -encode([{Key, Value} | Tail], Acc) -> - Encoded = [$", encode_key(Key), "\":", encode(Value), $,], - encode(Tail, [Acc | Encoded]); -encode([_V | _T] = L, []) -> - encode(L, $[); -encode([Value | []], Acc) -> - Encoded = [encode(Value), $]], - [Acc | Encoded]; -encode([Value | Tail], Acc) -> - Encoded = [encode(Value), $,], - encode(Tail, [Acc | Encoded]). - -encode_key(Key) when is_atom(Key) -> - erlang:atom_to_binary(Key, latin1); -encode_key(Key) when is_binary(Key) -> - Key. diff --git a/libs/estdlib/src/CMakeLists.txt b/libs/estdlib/src/CMakeLists.txt index ec51c569e5..1ed38c99f5 100644 --- a/libs/estdlib/src/CMakeLists.txt +++ b/libs/estdlib/src/CMakeLists.txt @@ -55,6 +55,7 @@ set(ERLANG_MODULES init io_lib io + json lists maps math diff --git a/libs/estdlib/src/json.erl b/libs/estdlib/src/json.erl new file mode 100644 index 0000000000..6b1e3109f4 --- /dev/null +++ b/libs/estdlib/src/json.erl @@ -0,0 +1,863 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% Licensed 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(json). + +%% Decoder API +-export([ + decode/1, + decode/3, + decode_start/3, + decode_continue/2 +]). + +%% Encoder API +-export([ + encode/1, + encode/2, + encode_value/2, + encode_atom/2, + encode_integer/1, + encode_float/1, + encode_list/2, + encode_map/2, + encode_map_checked/2, + encode_key_value_list/2, + encode_key_value_list_checked/2, + encode_binary/1, + encode_binary_escape_all/1 +]). + +%% Type exports +-export_type([ + encoder/0, + encode_value/0, + from_binary_fun/0, + array_start_fun/0, + array_push_fun/0, + array_finish_fun/0, + object_start_fun/0, + object_push_fun/0, + object_finish_fun/0, + decoders/0, + decode_value/0, + continuation_state/0 +]). + +%% +%% Types +%% + +-type encoder() :: fun((term(), encoder()) -> iodata()). + +-type encode_map(Value) :: #{binary() | atom() | integer() => Value}. + +-type encode_value() :: + integer() + | float() + | boolean() + | null + | binary() + | atom() + | [encode_value()] + | encode_map(encode_value()). + +-type from_binary_fun() :: fun((binary()) -> term()). + +-type array_start_fun() :: fun((Acc :: term()) -> ArrayAcc :: term()). +-type array_push_fun() :: fun((Value :: term(), Acc :: term()) -> NewAcc :: term()). +-type array_finish_fun() :: + fun((ArrayAcc :: term(), OldAcc :: term()) -> {term(), term()}). + +-type object_start_fun() :: fun((Acc :: term()) -> ObjectAcc :: term()). +-type object_push_fun() :: + fun((Key :: term(), Value :: term(), Acc :: term()) -> NewAcc :: term()). +-type object_finish_fun() :: + fun((ObjectAcc :: term(), OldAcc :: term()) -> {term(), term()}). + +-type decoders() :: + #{ + array_start => array_start_fun(), + array_push => array_push_fun(), + array_finish => array_finish_fun(), + object_start => object_start_fun(), + object_push => object_push_fun(), + object_finish => object_finish_fun(), + float => from_binary_fun(), + integer => from_binary_fun(), + string => from_binary_fun(), + null => term() + }. + +-type decode_value() :: + integer() + | float() + | boolean() + | null + | binary() + | [decode_value()] + | #{binary() => decode_value()}. + +-opaque continuation_state() :: {binary(), term(), tuple()}. + +-record(callbacks, { + array_start, + array_push, + array_finish, + object_start, + object_push, + object_finish, + float, + integer, + string, + null +}). + +%% +%% Public API - Decoder +%% + +%% The decoder uses an iterative stack-based approach. +%% Nesting depth is bounded only by available heap memory. +-spec decode(binary()) -> decode_value(). +decode(Bin) -> + {Value, ok, Rest} = decode(Bin, ok, #{}), + case skip_whitespace(Rest) of + <<>> -> Value; + <> -> error({invalid_byte, Byte}) + end. + +-spec decode(binary(), term(), decoders()) -> {Result :: term(), Acc :: term(), binary()}. +decode(Bin, Acc, Decoders) -> + case decode_start(Bin, Acc, Decoders) of + {Value, Acc1, Rest} -> {Value, Acc1, Rest}; + {continue, State} -> decode_continue(end_of_input, State) + end. + +-spec decode_start(binary(), term(), decoders()) -> + {Result :: term(), Acc :: term(), binary()} + | {continue, continuation_state()}. +decode_start(Bin, Acc, Decoders) -> + Callbacks = make_callbacks(Decoders), + try + Bin1 = skip_whitespace(Bin), + scan_value(Bin1, Acc, Callbacks, []) + catch + error:unexpected_end -> + {continue, {Bin, Acc, Callbacks}} + end. + +%% Note: each call reparses the accumulated buffer from the start. +%% For large payloads split across many small chunks, consider +%% accumulating the full binary before calling decode/1. +-spec decode_continue(binary() | end_of_input, State :: continuation_state()) -> + {Result :: term(), Acc :: term(), binary()} + | {continue, continuation_state()}. +decode_continue(end_of_input, {Buf, Acc, Callbacks}) -> + %% Append a space as delimiter to finalize any pending number, + %% then strip the trailing space from the result. + try + Bin1 = skip_whitespace(<>), + case scan_value(Bin1, Acc, Callbacks, []) of + {Result, Acc1, <<>>} -> {Result, Acc1, <<>>}; + {Result, Acc1, <<$\s>>} -> {Result, Acc1, <<>>}; + _ -> error(unexpected_end) + end + catch + error:_ -> + error(unexpected_end) + end; +decode_continue(NewData, {Buf, Acc, Callbacks}) -> + Combined = <>, + try + Bin1 = skip_whitespace(Combined), + scan_value(Bin1, Acc, Callbacks, []) + catch + error:unexpected_end -> + {continue, {Combined, Acc, Callbacks}} + end. + +%% +%% Internal: callback setup +%% + +make_callbacks(Decoders) -> + #callbacks{ + array_start = maps:get(array_start, Decoders, fun default_array_start/1), + array_push = maps:get(array_push, Decoders, fun default_array_push/2), + array_finish = maps:get(array_finish, Decoders, fun default_array_finish/2), + object_start = maps:get(object_start, Decoders, fun default_object_start/1), + object_push = maps:get(object_push, Decoders, fun default_object_push/3), + object_finish = maps:get(object_finish, Decoders, fun default_object_finish/2), + float = maps:get(float, Decoders, fun erlang:binary_to_float/1), + integer = maps:get(integer, Decoders, fun erlang:binary_to_integer/1), + string = maps:get(string, Decoders, fun default_string/1), + null = maps:get(null, Decoders, null) + }. + +default_array_start(_) -> []. +default_array_push(Elem, Acc) -> [Elem | Acc]. +default_array_finish(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc}. +default_object_start(_) -> []. +default_object_push(Key, Value, Acc) -> [{Key, Value} | Acc]. +default_object_finish(Acc, OldAcc) -> {maps:from_list(Acc), OldAcc}. +default_string(V) -> binary:copy(V). + +%% +%% Internal: whitespace +%% + +skip_whitespace(<<$\s, Rest/binary>>) -> skip_whitespace(Rest); +skip_whitespace(<<$\t, Rest/binary>>) -> skip_whitespace(Rest); +skip_whitespace(<<$\n, Rest/binary>>) -> skip_whitespace(Rest); +skip_whitespace(<<$\r, Rest/binary>>) -> skip_whitespace(Rest); +skip_whitespace(Bin) -> Bin. + +%% +%% Internal: iterative value scanner +%% +%% Uses an explicit stack (list of frames) instead of the Erlang call stack. +%% Callbacks are extracted from the tuple on use, keeping frames small. +%% Stack frame types: +%% {array, OldAcc} +%% {object, Key, OldAcc} + +scan_value(<<"true", Rest/binary>>, Acc, Callbacks, Stack) -> + reduce_value(true, Rest, Acc, Callbacks, Stack); +scan_value(<<"false", Rest/binary>>, Acc, Callbacks, Stack) -> + reduce_value(false, Rest, Acc, Callbacks, Stack); +scan_value(<<"null", Rest/binary>>, Acc, Callbacks, Stack) -> + reduce_value(Callbacks#callbacks.null, Rest, Acc, Callbacks, Stack); +scan_value(<<$", Rest/binary>>, Acc, Callbacks, Stack) -> + {Value, Acc1, Rest1} = string_fast(Rest, Rest, Acc, Callbacks), + reduce_value(Value, Rest1, Acc1, Callbacks, Stack); +scan_value(<<$[, Rest/binary>>, Acc, Callbacks, Stack) -> + ArrayAcc = (Callbacks#callbacks.array_start)(Acc), + Rest1 = skip_whitespace(Rest), + case Rest1 of + <<$], Rest2/binary>> -> + {Result, NewAcc} = (Callbacks#callbacks.array_finish)(ArrayAcc, Acc), + reduce_value(Result, Rest2, NewAcc, Callbacks, Stack); + _ -> + scan_value(Rest1, ArrayAcc, Callbacks, [{array, Acc} | Stack]) + end; +scan_value(<<${, Rest/binary>>, Acc, Callbacks, Stack) -> + ObjAcc = (Callbacks#callbacks.object_start)(Acc), + Rest1 = skip_whitespace(Rest), + case Rest1 of + <<$}, Rest2/binary>> -> + {Result, NewAcc} = (Callbacks#callbacks.object_finish)(ObjAcc, Acc), + reduce_value(Result, Rest2, NewAcc, Callbacks, Stack); + <<$", Rest2/binary>> -> + {Key, ObjAcc1, Rest3} = string_fast(Rest2, Rest2, ObjAcc, Callbacks), + Rest4 = skip_whitespace(Rest3), + case Rest4 of + <<$:, Rest5/binary>> -> + Rest6 = skip_whitespace(Rest5), + scan_value(Rest6, ObjAcc1, Callbacks, [{object, Key, Acc} | Stack]); + <> -> + error({invalid_byte, Byte}); + <<>> -> + error(unexpected_end) + end; + <> -> + error({invalid_byte, Byte}); + <<>> -> + error(unexpected_end) + end; +scan_value(<<$-, _/binary>> = Bin, Acc, Callbacks, Stack) -> + {Value, Acc1, Rest} = parse_number(Bin, Acc, Callbacks), + reduce_value(Value, Rest, Acc1, Callbacks, Stack); +scan_value(<> = Bin, Acc, Callbacks, Stack) when D >= $0, D =< $9 -> + {Value, Acc1, Rest} = parse_number(Bin, Acc, Callbacks), + reduce_value(Value, Rest, Acc1, Callbacks, Stack); +%% Partial literals (for streaming: input may be truncated mid-keyword) +scan_value(<<$t, _/binary>> = Bin, _, _, _) -> + maybe_partial_literal(Bin, <<"true">>, $t); +scan_value(<<$f, _/binary>> = Bin, _, _, _) -> + maybe_partial_literal(Bin, <<"false">>, $f); +scan_value(<<$n, _/binary>> = Bin, _, _, _) -> + maybe_partial_literal(Bin, <<"null">>, $n); +scan_value(<>, _, _, _) -> + error({invalid_byte, Byte}); +scan_value(<<>>, _, _, _) -> + error(unexpected_end). + +%% After parsing a value, attach it to the parent container or return at top level. +%% Match OTP: consume trailing whitespace only when rest is whitespace-only. +reduce_value(Value, Rest, Acc, _Callbacks, []) -> + case skip_whitespace(Rest) of + <<>> -> {Value, Acc, <<>>}; + _ -> {Value, Acc, Rest} + end; +reduce_value(Value, Rest0, Acc, Callbacks, [{array, OldAcc} | StackRest] = Stack) -> + ArrayAcc = (Callbacks#callbacks.array_push)(Value, Acc), + Rest1 = skip_whitespace(Rest0), + case Rest1 of + <<$], Rest2/binary>> -> + {Result, NewAcc} = (Callbacks#callbacks.array_finish)(ArrayAcc, OldAcc), + reduce_value(Result, Rest2, NewAcc, Callbacks, StackRest); + <<$,, Rest2/binary>> -> + Rest3 = skip_whitespace(Rest2), + scan_value(Rest3, ArrayAcc, Callbacks, Stack); + <> -> + error({invalid_byte, Byte}); + <<>> -> + error(unexpected_end) + end; +reduce_value(Value, Rest0, Acc, Callbacks, [{object, Key, OldAcc} | StackRest]) -> + ObjAcc = (Callbacks#callbacks.object_push)(Key, Value, Acc), + Rest1 = skip_whitespace(Rest0), + case Rest1 of + <<$}, Rest2/binary>> -> + {Result, NewAcc} = (Callbacks#callbacks.object_finish)(ObjAcc, OldAcc), + reduce_value(Result, Rest2, NewAcc, Callbacks, StackRest); + <<$,, Rest2/binary>> -> + Rest3 = skip_whitespace(Rest2), + case Rest3 of + <<$", Rest4/binary>> -> + {NewKey, ObjAcc1, Rest5} = string_fast(Rest4, Rest4, ObjAcc, Callbacks), + Rest6 = skip_whitespace(Rest5), + case Rest6 of + <<$:, Rest7/binary>> -> + Rest8 = skip_whitespace(Rest7), + scan_value( + Rest8, + ObjAcc1, + Callbacks, + [{object, NewKey, OldAcc} | StackRest] + ); + <> -> + error({invalid_byte, Byte}); + <<>> -> + error(unexpected_end) + end; + <> -> + error({invalid_byte, Byte}); + <<>> -> + error(unexpected_end) + end; + <> -> + error({invalid_byte, Byte}); + <<>> -> + error(unexpected_end) + end. + +maybe_partial_literal(Bin, Expected, Byte) -> + Len = byte_size(Bin), + case Expected of + <> when Prefix =:= Bin -> error(unexpected_end); + _ -> error({invalid_byte, Byte}) + end. + +%% +%% Internal: string parser +%% + +%% Fast path: no escapes. Walk with /utf8 matching for automatic UTF-8 validation. +%% Orig = binary starting after opening quote (for binary_part extraction). +%% Position computed from byte_size at $" or $\ -- no Pos counter needed. +string_fast(<<$", Rest/binary>>, Orig, Acc, Callbacks) -> + Pos = byte_size(Orig) - byte_size(Rest) - 1, + Str = binary_part(Orig, 0, Pos), + apply_string_decoder(Str, Acc, Rest, Callbacks); +string_fast(<<$\\, Rest/binary>>, Orig, Acc, Callbacks) -> + Pos = byte_size(Orig) - byte_size(Rest) - 1, + Prefix = binary_part(Orig, 0, Pos), + string_escape(Rest, [Prefix], Acc, Callbacks); +string_fast(<>, _, _, _) when Ch < 16#20 -> + error({invalid_byte, Ch}); +string_fast(<<_/utf8, Rest/binary>>, Orig, Acc, Callbacks) -> + string_fast(Rest, Orig, Acc, Callbacks); +%% Maybe truncated UTF-8 sequence at end of buffer. Could continue +string_fast(<>, _, _, _) when + byte_size(Rest) < 3, + (B >= 16#C0 andalso B < 16#E0 andalso byte_size(Rest) < 1) orelse + (B >= 16#E0 andalso B < 16#F0 andalso byte_size(Rest) < 2) orelse + (B >= 16#F0 andalso B < 16#F8 andalso byte_size(Rest) < 3) +-> + error(unexpected_end); +%% Invalid UTF-8 byte (not matched by /utf8 above) +string_fast(<>, _, _, _) -> + error({invalid_byte, B}); +string_fast(<<>>, _, _, _) -> + error(unexpected_end). + +apply_string_decoder(Str, Acc, Rest, Callbacks) -> + StringFun = Callbacks#callbacks.string, + {StringFun(Str), Acc, Rest}. + +%% Slow path: has escapes. Accumulate decoded chunks in reversed iolist. +%% Enter a new "run" scanning for the next escape or closing quote. +string_slow(Bin, Parts, Acc, Callbacks) -> + string_slow_run(Bin, Bin, Parts, Acc, Callbacks). + +string_slow_run(<<$", Rest/binary>>, RunOrig, Parts, Acc, Callbacks) -> + RunLen = byte_size(RunOrig) - byte_size(Rest) - 1, + Run = binary_part(RunOrig, 0, RunLen), + Str = iolist_to_binary(lists:reverse([Run | Parts])), + apply_string_decoder(Str, Acc, Rest, Callbacks); +string_slow_run(<<$\\, Rest/binary>>, RunOrig, Parts, Acc, Callbacks) -> + RunLen = byte_size(RunOrig) - byte_size(Rest) - 1, + Run = binary_part(RunOrig, 0, RunLen), + string_escape(Rest, [Run | Parts], Acc, Callbacks); +string_slow_run(<>, _, _, _, _) when Ch < 16#20 -> + error({invalid_byte, Ch}); +string_slow_run(<<_/utf8, Rest/binary>>, RunOrig, Parts, Acc, Callbacks) -> + string_slow_run(Rest, RunOrig, Parts, Acc, Callbacks); +%% Maybe truncated UTF-8 sequence at end of buffer. Could continue +string_slow_run(<>, _, _, _, _) when + byte_size(Rest) < 3, + (B >= 16#C0 andalso B < 16#E0 andalso byte_size(Rest) < 1) orelse + (B >= 16#E0 andalso B < 16#F0 andalso byte_size(Rest) < 2) orelse + (B >= 16#F0 andalso B < 16#F8 andalso byte_size(Rest) < 3) +-> + error(unexpected_end); +string_slow_run(<>, _, _, _, _) -> + error({invalid_byte, B}); +string_slow_run(<<>>, _, _, _, _) -> + error(unexpected_end). + +%% Escape character dispatch +string_escape(<<$", Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$">> | Parts], Acc, Callbacks); +string_escape(<<$\\, Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$\\>> | Parts], Acc, Callbacks); +string_escape(<<$/, Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$/>> | Parts], Acc, Callbacks); +string_escape(<<$b, Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$\b>> | Parts], Acc, Callbacks); +string_escape(<<$f, Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$\f>> | Parts], Acc, Callbacks); +string_escape(<<$n, Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$\n>> | Parts], Acc, Callbacks); +string_escape(<<$r, Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$\r>> | Parts], Acc, Callbacks); +string_escape(<<$t, Rest/binary>>, Parts, Acc, Callbacks) -> + string_slow(Rest, [<<$\t>> | Parts], Acc, Callbacks); +string_escape(<<$u, Rest/binary>>, Parts, Acc, Callbacks) -> + parse_unicode_escape(Rest, Parts, Acc, Callbacks); +string_escape(<>, _, _, _) -> + error({invalid_byte, Byte}); +string_escape(<<>>, _, _, _) -> + error(unexpected_end). + +%% Unicode escape: \uXXXX with surrogate pair support +parse_unicode_escape(<>, Parts, Acc, Callbacks) -> + case safe_hex4(H1, H2, H3, H4) of + error -> + error({unexpected_sequence, <<$\\, $u, H1, H2, H3, H4>>}); + CP when CP >= 16#D800, CP =< 16#DBFF -> + %% High surrogate - must be followed by low surrogate \uDC00-\uDFFF + parse_surrogate_low(Rest, CP, H1, H2, H3, H4, Parts, Acc, Callbacks); + CP when CP >= 16#DC00, CP =< 16#DFFF -> + %% Lone low surrogate + error({unexpected_sequence, <<$\\, $u, H1, H2, H3, H4>>}); + CP -> + Utf8 = <>, + string_slow(Rest, [Utf8 | Parts], Acc, Callbacks) + end; +parse_unicode_escape(_, _, _, _) -> + error(unexpected_end). + +parse_surrogate_low( + <<$\\, $u, L1, L2, L3, L4, Rest/binary>>, High, H1, H2, H3, H4, Parts, Acc, Callbacks +) -> + case safe_hex4(L1, L2, L3, L4) of + error -> + error( + {unexpected_sequence, <<$\\, $u, H1, H2, H3, H4, $\\, $u, L1, L2, L3, L4>>} + ); + Low when Low >= 16#DC00, Low =< 16#DFFF -> + Combined = 16#10000 + ((High - 16#D800) bsl 10) + (Low - 16#DC00), + Utf8 = <>, + string_slow(Rest, [Utf8 | Parts], Acc, Callbacks); + _Low -> + error( + {unexpected_sequence, <<$\\, $u, H1, H2, H3, H4, $\\, $u, L1, L2, L3, L4>>} + ) + end; +parse_surrogate_low(<<>>, _, _H1, _H2, _H3, _H4, _, _, _) -> + error(unexpected_end); +parse_surrogate_low(<<$\\>>, _, _H1, _H2, _H3, _H4, _, _, _) -> + error(unexpected_end); +parse_surrogate_low(<<$\\, $u, _/binary>>, _, _H1, _H2, _H3, _H4, _, _, _) -> + %% \u present but fewer than 4 hex digits + error(unexpected_end); +parse_surrogate_low(_, _, H1, H2, H3, H4, _, _, _) -> + error({unexpected_sequence, <<$\\, $u, H1, H2, H3, H4>>}). + +safe_hex4(H1, H2, H3, H4) -> + case {safe_hex(H1), safe_hex(H2), safe_hex(H3), safe_hex(H4)} of + {V1, V2, V3, V4} when + is_integer(V1), + is_integer(V2), + is_integer(V3), + is_integer(V4) + -> + (V1 bsl 12) bor (V2 bsl 8) bor (V3 bsl 4) bor V4; + _ -> + error + end. + +safe_hex(D) when D >= $0, D =< $9 -> D - $0; +safe_hex(D) when D >= $a, D =< $f -> D - $a + 10; +safe_hex(D) when D >= $A, D =< $F -> D - $A + 10; +safe_hex(_) -> error. + +%% +%% Internal: number parser +%% +%% State machine following RFC 8259 Section 6: +%% number = [ minus ] int [ frac ] [ exp ] +%% int = zero / ( digit1-9 *DIGIT ) +%% frac = decimal-point 1*DIGIT +%% exp = e [ minus / plus ] 1*DIGIT + +parse_number(<<$-, Rest/binary>> = Bin, Acc, Callbacks) -> + number_minus(Rest, Bin, 1, Acc, Callbacks); +parse_number(<<$0, Rest/binary>> = Bin, Acc, Callbacks) -> + number_zero(Rest, Bin, 1, Acc, Callbacks); +parse_number(<> = Bin, Acc, Callbacks) when D >= $1, D =< $9 -> + number_digits(Rest, Bin, 1, Acc, Callbacks). + +%% After minus: need at least one digit +number_minus(<<$0, Rest/binary>>, Bin, Pos, Acc, Callbacks) -> + number_zero(Rest, Bin, Pos + 1, Acc, Callbacks); +number_minus(<>, Bin, Pos, Acc, Callbacks) when D >= $1, D =< $9 -> + number_digits(Rest, Bin, Pos + 1, Acc, Callbacks); +number_minus(<>, _, _, _, _) -> + error({invalid_byte, Byte}); +number_minus(<<>>, _, _, _, _) -> + error(unexpected_end). + +%% After leading zero: only dot, exponent, or end allowed (no leading zeros like 01) +number_zero(<<$., Rest/binary>>, Bin, Pos, Acc, Callbacks) -> + number_frac_first(Rest, Bin, Pos + 1, Acc, Callbacks); +number_zero(<>, Bin, Pos, Acc, Callbacks) when E =:= $e; E =:= $E -> + number_exp_sign(Rest, Bin, Pos + 1, float_exp, Acc, Callbacks); +number_zero(<<>>, _, _, _, _) -> + error(unexpected_end); +number_zero(Rest, Bin, Pos, Acc, Callbacks) -> + number_complete(Rest, Bin, Pos, integer, Acc, Callbacks). + +%% Integer digits (first digit was 1-9) +number_digits(<>, Bin, Pos, Acc, Callbacks) when D >= $0, D =< $9 -> + number_digits(Rest, Bin, Pos + 1, Acc, Callbacks); +number_digits(<<$., Rest/binary>>, Bin, Pos, Acc, Callbacks) -> + number_frac_first(Rest, Bin, Pos + 1, Acc, Callbacks); +number_digits(<>, Bin, Pos, Acc, Callbacks) when E =:= $e; E =:= $E -> + number_exp_sign(Rest, Bin, Pos + 1, float_exp, Acc, Callbacks); +number_digits(<<>>, _, _, _, _) -> + error(unexpected_end); +number_digits(Rest, Bin, Pos, Acc, Callbacks) -> + number_complete(Rest, Bin, Pos, integer, Acc, Callbacks). + +%% After dot: need at least one digit +number_frac_first(<>, Bin, Pos, Acc, Callbacks) when D >= $0, D =< $9 -> + number_frac(Rest, Bin, Pos + 1, Acc, Callbacks); +number_frac_first(<>, _, _, _, _) -> + error({invalid_byte, Byte}); +number_frac_first(<<>>, _, _, _, _) -> + error(unexpected_end). + +%% Fraction digits +number_frac(<>, Bin, Pos, Acc, Callbacks) when D >= $0, D =< $9 -> + number_frac(Rest, Bin, Pos + 1, Acc, Callbacks); +number_frac(<>, Bin, Pos, Acc, Callbacks) when E =:= $e; E =:= $E -> + number_exp_sign(Rest, Bin, Pos + 1, float, Acc, Callbacks); +number_frac(<<>>, _, _, _, _) -> + error(unexpected_end); +number_frac(Rest, Bin, Pos, Acc, Callbacks) -> + number_complete(Rest, Bin, Pos, float, Acc, Callbacks). + +%% Exponent: optional sign, then digits +%% Type = float (had dot) | float_exp (no dot, needs normalization) +number_exp_sign(<>, Bin, Pos, Type, Acc, Callbacks) when S =:= $+; S =:= $- -> + number_exp_first(Rest, Bin, Pos + 1, Type, Acc, Callbacks); +number_exp_sign(Rest, Bin, Pos, Type, Acc, Callbacks) -> + number_exp_first(Rest, Bin, Pos, Type, Acc, Callbacks). + +%% After e/E [+/-]: need at least one digit +number_exp_first(<>, Bin, Pos, Type, Acc, Callbacks) when D >= $0, D =< $9 -> + number_exp_digits(Rest, Bin, Pos + 1, Type, Acc, Callbacks); +number_exp_first(<>, _, _, _, _, _) -> + error({invalid_byte, Byte}); +number_exp_first(<<>>, _, _, _, _, _) -> + error(unexpected_end). + +%% Exponent digits +number_exp_digits(<>, Bin, Pos, Type, Acc, Callbacks) when D >= $0, D =< $9 -> + number_exp_digits(Rest, Bin, Pos + 1, Type, Acc, Callbacks); +number_exp_digits(<<>>, _, _, _, _, _) -> + error(unexpected_end); +number_exp_digits(Rest, Bin, Pos, Type, Acc, Callbacks) -> + number_complete(Rest, Bin, Pos, Type, Acc, Callbacks). + +%% Extract number binary and convert +number_complete(Rest, Orig, Pos, integer, Acc, Callbacks) -> + NumBin = binary_part(Orig, 0, Pos), + IntFun = Callbacks#callbacks.integer, + {IntFun(NumBin), Acc, Rest}; +number_complete(Rest, Orig, Pos, float, Acc, Callbacks) -> + NumBin = binary_part(Orig, 0, Pos), + FloatFun = Callbacks#callbacks.float, + {FloatFun(NumBin), Acc, Rest}; +number_complete(Rest, Orig, Pos, float_exp, Acc, Callbacks) -> + NumBin = binary_part(Orig, 0, Pos), + NormBin = normalize_float(NumBin), + FloatFun = Callbacks#callbacks.float, + {FloatFun(NormBin), Acc, Rest}. + +%% Insert ".0" before e/E for numbers like "1e5" -> "1.0e5" +%% (Erlang's binary_to_float requires a decimal point) +normalize_float(Bin) -> + normalize_float(Bin, Bin, 0). + +normalize_float(<>, Orig, Pos) when E =:= $e; E =:= $E -> + IntPart = binary_part(Orig, 0, Pos), + ExpPart = binary_part(Orig, Pos, byte_size(Orig) - Pos), + iolist_to_binary([IntPart, ".0", ExpPart]); +normalize_float(<<_, Rest/binary>>, Orig, Pos) -> + normalize_float(Rest, Orig, Pos + 1). + +%% +%% Encoder +%% + +-spec encode(encode_value()) -> iodata(). +encode(Term) -> + encode(Term, fun encode_value/2). + +-spec encode(term(), encoder()) -> iodata(). +encode(Term, Encoder) -> + Encoder(Term, Encoder). + +-spec encode_value(term(), encoder()) -> iodata(). +encode_value(Atom, Encoder) when is_atom(Atom) -> encode_atom(Atom, Encoder); +encode_value(Bin, _Encoder) when is_binary(Bin) -> encode_binary(Bin); +encode_value(Int, _Encoder) when is_integer(Int) -> encode_integer(Int); +encode_value(Float, _Encoder) when is_float(Float) -> encode_float(Float); +encode_value(List, Encoder) when is_list(List) -> encode_list(List, Encoder); +encode_value(Map, Encoder) when is_map(Map) -> encode_map(Map, Encoder); +encode_value(Other, _Encoder) -> error({unsupported_type, Other}). + +-spec encode_atom(atom(), encoder()) -> iodata(). +encode_atom(null, _) -> <<"null">>; +encode_atom(true, _) -> <<"true">>; +encode_atom(false, _) -> <<"false">>; +encode_atom(Atom, Encoder) -> Encoder(atom_to_binary(Atom, utf8), Encoder). + +-spec encode_integer(integer()) -> iodata(). +encode_integer(Int) -> + integer_to_binary(Int). + +-spec encode_float(float()) -> iodata(). +encode_float(Float) -> + float_to_binary(Float, [short]). + +-spec encode_list(list(), encoder()) -> iodata(). +encode_list([], _Encoder) -> + <<"[]">>; +encode_list([H | T], Encoder) -> + [$[, Encoder(H, Encoder) | encode_list_loop(T, Encoder)]. + +encode_list_loop([], _) -> [$]]; +encode_list_loop([H | T], Encoder) -> [$,, Encoder(H, Encoder) | encode_list_loop(T, Encoder)]. + +%% +%% Encoder: string encoding +%% + +-spec encode_binary(binary()) -> iodata(). +encode_binary(Bin) -> + [$", escape(Bin, Bin, [], false), $"]. + +-spec encode_binary_escape_all(binary()) -> iodata(). +encode_binary_escape_all(Bin) -> + [$", escape(Bin, Bin, [], true), $"]. + +%% Shared escape scanner. +%% Orig tracks start of current unescaped run (for binary_part extraction). +%% Acc = [] means fast path (no escapes found yet); non-[] means building iolist. +%% EscapeAll: true escapes non-ASCII (>= 0x80) to \uXXXX, false passes through. +escape(<<>>, Orig, [], _) -> + Orig; +escape(<<>>, RunOrig, Acc, _) -> + lists:reverse([RunOrig | Acc]); +escape(<<$", Rest/binary>>, Orig, Acc, EA) -> + escape_found(Orig, Rest, <<"\\\"">>, Acc, EA); +escape(<<$\\, Rest/binary>>, Orig, Acc, EA) -> + escape_found(Orig, Rest, <<"\\\\">>, Acc, EA); +escape(<>, Orig, Acc, EA) when Ch < 16#20 -> + escape_found(Orig, Rest, escape_ctrl(Ch), Acc, EA); +escape(<>, Orig, Acc, true) when Ch >= 16#80 -> + Pos = byte_size(Orig) - byte_size(Rest) - utf8_byte_len(Ch), + Run = binary_part(Orig, 0, Pos), + escape(Rest, Rest, [escape_unicode(Ch), Run | Acc], true); +escape(<<_/utf8, Rest/binary>>, Orig, Acc, EA) -> + escape(Rest, Orig, Acc, EA); +%% Maybe truncated UTF-8 sequence at end of binary +escape(<>, _, _, _) when + byte_size(Rest) < 3, + (B >= 16#C0 andalso B < 16#E0 andalso byte_size(Rest) < 1) orelse + (B >= 16#E0 andalso B < 16#F0 andalso byte_size(Rest) < 2) orelse + (B >= 16#F0 andalso B < 16#F8 andalso byte_size(Rest) < 3) +-> + error(unexpected_end); +escape(<>, _, _, _) -> + error({invalid_byte, B}). + +escape_found(Orig, Rest, Replacement, Acc, EA) -> + Pos = byte_size(Orig) - byte_size(Rest) - 1, + Run = binary_part(Orig, 0, Pos), + escape(Rest, Rest, [Replacement, Run | Acc], EA). + +escape_ctrl($\b) -> <<"\\b">>; +escape_ctrl($\t) -> <<"\\t">>; +escape_ctrl($\n) -> <<"\\n">>; +escape_ctrl($\f) -> <<"\\f">>; +escape_ctrl($\r) -> <<"\\r">>; +escape_ctrl(Ch) -> <<"\\u00", (hex_char(Ch bsr 4)), (hex_char(Ch band 16#F))>>. + +escape_unicode(Ch) when Ch < 16#10000 -> + << + $\\, + $u, + (hex_char(Ch bsr 12)), + (hex_char((Ch bsr 8) band 16#F)), + (hex_char((Ch bsr 4) band 16#F)), + (hex_char(Ch band 16#F)) + >>; +escape_unicode(Ch) -> + Ch1 = Ch - 16#10000, + High = 16#D800 + (Ch1 bsr 10), + Low = 16#DC00 + (Ch1 band 16#3FF), + << + $\\, + $u, + (hex_char(High bsr 12)), + (hex_char((High bsr 8) band 16#F)), + (hex_char((High bsr 4) band 16#F)), + (hex_char(High band 16#F)), + $\\, + $u, + (hex_char(Low bsr 12)), + (hex_char((Low bsr 8) band 16#F)), + (hex_char((Low bsr 4) band 16#F)), + (hex_char(Low band 16#F)) + >>. + +utf8_byte_len(Ch) when Ch < 16#800 -> 2; +utf8_byte_len(Ch) when Ch < 16#10000 -> 3; +utf8_byte_len(_) -> 4. + +hex_char(N) when N < 10 -> $0 + N; +hex_char(N) -> $A + N - 10. + +%% +%% Encoder: object encoding (maps and key-value lists) +%% + +-spec encode_map(encode_map(term()), encoder()) -> iodata(). +encode_map(Map, Encoder) -> + encode_object(fun maps:next/1, maps:iterator(Map), Encoder). + +-spec encode_key_value_list([{term(), term()}], encoder()) -> iodata(). +encode_key_value_list(List, Encoder) -> + encode_object(fun kv_next/1, List, Encoder). + +-spec encode_map_checked(map(), encoder()) -> iodata(). +encode_map_checked(Map, Encoder) -> + encode_object_checked(fun maps:next/1, maps:iterator(Map), Encoder). + +-spec encode_key_value_list_checked([{term(), term()}], encoder()) -> iodata(). +encode_key_value_list_checked(List, Encoder) -> + encode_object_checked(fun kv_next/1, List, Encoder). + +encode_object(IterFun, State, Encoder) -> + case IterFun(State) of + none -> + <<"{}">>; + {Key, Value, Next} -> + [ + ${, + encode_key(Key, Encoder), + $:, + Encoder(Value, Encoder) + | encode_object_rest(IterFun, Next, Encoder) + ] + end. + +encode_object_rest(IterFun, State, Encoder) -> + case IterFun(State) of + none -> + [$}]; + {Key, Value, Next} -> + [ + $,, + encode_key(Key, Encoder), + $:, + Encoder(Value, Encoder) + | encode_object_rest(IterFun, Next, Encoder) + ] + end. + +encode_object_checked(IterFun, State, Encoder) -> + case IterFun(State) of + none -> + <<"{}">>; + {Key, Value, Next} -> + EncodedKey = iolist_to_binary(encode_key(Key, Encoder)), + [ + ${, + EncodedKey, + $:, + Encoder(Value, Encoder) + | encode_object_rest_checked(IterFun, Next, Encoder, #{EncodedKey => Key}) + ] + end. + +encode_object_rest_checked(IterFun, State, Encoder, Seen) -> + case IterFun(State) of + none -> + [$}]; + {Key, Value, Next} -> + EncodedKey = iolist_to_binary(encode_key(Key, Encoder)), + case Seen of + #{EncodedKey := _} -> error({duplicate_key, Key}); + _ -> ok + end, + [ + $,, + EncodedKey, + $:, + Encoder(Value, Encoder) + | encode_object_rest_checked(IterFun, Next, Encoder, Seen#{EncodedKey => Key}) + ] + end. + +encode_key(Key, Encoder) when is_binary(Key) -> Encoder(Key, Encoder); +encode_key(Key, Encoder) when is_atom(Key) -> Encoder(atom_to_binary(Key, utf8), Encoder); +encode_key(Key, _Encoder) when is_integer(Key) -> encode_binary(integer_to_binary(Key)); +encode_key(Key, _Encoder) when is_float(Key) -> encode_binary(float_to_binary(Key, [short])). + +kv_next([]) -> none; +kv_next([{K, V} | T]) -> {K, V, T}. diff --git a/libs/exavmlib/lib/CMakeLists.txt b/libs/exavmlib/lib/CMakeLists.txt index f5b895c05a..165a9926a9 100644 --- a/libs/exavmlib/lib/CMakeLists.txt +++ b/libs/exavmlib/lib/CMakeLists.txt @@ -98,7 +98,22 @@ set(ELIXIR_MODULES String.Chars.List ) -pack_archive(exavmlib ${ELIXIR_MODULES}) +compile_multi(json.ex + JSON + JSON.DecodeError + JSON.Encoder + JSON.Encoder.Atom + JSON.Encoder.BitString + JSON.Encoder.Float + JSON.Encoder.Integer + JSON.Encoder.List + JSON.Encoder.Map + OUTPUT JSON_BEAMS +) + +pack_archive(exavmlib ${ELIXIR_MODULES} + EXTRA_BEAMS ${JSON_BEAMS} +) include(../../../version.cmake) diff --git a/libs/exavmlib/lib/Keyword.ex b/libs/exavmlib/lib/Keyword.ex index bcb11092a8..94210347dc 100644 --- a/libs/exavmlib/lib/Keyword.ex +++ b/libs/exavmlib/lib/Keyword.ex @@ -7,6 +7,9 @@ # merge/2 take/2 pop/2/3 pop!/2 keyword?/1 has_key?/2 split/2 from: # https://github.com/elixir-lang/elixir/blob/v1.16/lib/elixir/lib/keyword.ex # +# put_new/3 from: +# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/keyword.ex +# # Licensed 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 @@ -94,6 +97,13 @@ defmodule Keyword do [{key, value} | delete(keywords, key)] end + def put_new(keywords, key, value) when is_list(keywords) and is_atom(key) do + case :lists.keyfind(key, 1, keywords) do + {^key, _} -> keywords + false -> [{key, value} | keywords] + end + end + def delete(keywords, key) when is_list(keywords) and is_atom(key) do case :lists.keymember(key, 1, keywords) do true -> delete_key(keywords, key) diff --git a/libs/exavmlib/lib/json.ex b/libs/exavmlib/lib/json.ex new file mode 100644 index 0000000000..d53743b0a7 --- /dev/null +++ b/libs/exavmlib/lib/json.ex @@ -0,0 +1,316 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2021 The Elixir Team +# https://github.com/elixir-lang/elixir/blob/03b9fde6/lib/elixir/lib/json.ex +# +# Licensed 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. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Adapted for AtomVM: +# - Removed __deriving__ macro (not supported) +# - Removed Duration, Date, Time, NaiveDateTime, DateTime encoders +# (Calendar modules not available) +# - Simplified error offset extraction (error_info not supported) +# + +defprotocol JSON.Encoder do + # Elixir < 1.18.0 doesn't have a built-in JSON.Encoder protocol, + # so autoload must remain enabled for defimpl to find it. + if Version.match?(System.version(), ">= 1.18.0") do + @compile {:autoload, false} + end + + @moduledoc """ + A protocol for custom JSON encoding of data structures. + """ + + @doc """ + A function invoked to encode the given term to `t:iodata/0`. + """ + def encode(term, encoder) +end + +defimpl JSON.Encoder, for: Atom do + def encode(value, encoder) do + case value do + nil -> "null" + true -> "true" + false -> "false" + _ -> encoder.(Atom.to_string(value), encoder) + end + end +end + +defimpl JSON.Encoder, for: BitString do + def encode(value, _encoder) do + :json.encode_binary(value) + end +end + +defimpl JSON.Encoder, for: List do + def encode(value, encoder) do + :json.encode_list(value, encoder) + end +end + +defimpl JSON.Encoder, for: Integer do + def encode(value, _encoder) do + :json.encode_integer(value) + end +end + +defimpl JSON.Encoder, for: Float do + def encode(value, _encoder) do + :json.encode_float(value) + end +end + +defimpl JSON.Encoder, for: Map do + def encode(value, encoder) do + case :maps.next(:maps.iterator(value)) do + :none -> + "{}" + + {key, value, iterator} -> + [?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)] + end + end + + defp next(iterator, encoder) do + case :maps.next(iterator) do + :none -> + "}" + + {key, value, iterator} -> + [?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)] + end + end + + defp key(key, encoder) when is_atom(key), + do: encoder.(Atom.to_string(key), encoder) + + defp key(key, encoder) when is_binary(key), + do: encoder.(key, encoder) + + defp key(key, encoder), + do: encoder.(String.Chars.to_string(key), encoder) +end + +defmodule JSON.DecodeError do + @compile {:autoload, false} + + @moduledoc """ + The exception raised by `JSON.decode!/1`. + """ + defexception [:message, :offset, :data] +end + +defmodule JSON do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + @moduledoc ~S""" + JSON encoding and decoding. + + Both encoder and decoder fully conform to + [RFC 8259](https://tools.ietf.org/html/rfc8259) and + [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) + standards. + + > **AtomVM limitation:** Decode error offsets are not tracked. + > `{:error, {:invalid_byte, offset, byte}}` and similar tuples + > always report offset `0`. + + ## Encoding + + | **Elixir** | **JSON** | + |----------------------------|----------| + | `integer() \| float()` | Number | + | `true \| false ` | Boolean | + | `nil` | Null | + | `binary()` | String | + | `atom()` | String | + | `list()` | Array | + | `%{String.Chars.t() => _}` | Object | + + ## Decoding + + | **JSON** | **Elixir** | + |----------|------------------------| + | Number | `integer() \| float()` | + | Boolean | `true \| false` | + | Null | `nil` | + | String | `binary()` | + | Object | `%{binary() => _}` | + """ + + @doc ~S""" + Decodes the given JSON. + + Returns `{:ok, decoded}` or `{:error, reason}`. + + ## Examples + + iex> JSON.decode("[null,123,\"string\",{\"key\":\"value\"}]") + {:ok, [nil, 123, "string", %{"key" => "value"}]} + """ + @spec decode(binary()) :: {:ok, term()} | {:error, term()} + def decode(binary) when is_binary(binary) do + with {decoded, :ok, rest} <- decode(binary, :ok, []) do + if rest == "" do + {:ok, decoded} + else + {:error, {:invalid_byte, byte_size(binary) - byte_size(rest), :binary.at(rest, 0)}} + end + end + end + + @doc ~S""" + Decodes the given JSON with the given decoders. + + Returns `{decoded, acc, rest}` or `{:error, reason}`. + + All decoders are optional. If not provided, they will fall back to + implementations used by the `decode/1` function. + """ + @spec decode(binary(), term(), keyword()) :: + {term(), term(), binary()} | {:error, term()} + def decode(binary, acc, decoders) + when is_binary(binary) and is_list(decoders) do + decoders = Keyword.put_new(decoders, :null, nil) + + try do + :json.decode(binary, acc, Map.new(decoders)) + catch + :error, :unexpected_end -> + {:error, {:unexpected_end, byte_size(binary)}} + + :error, {:invalid_byte, byte} -> + {:error, {:invalid_byte, 0, byte}} + + :error, {:unexpected_sequence, bytes} -> + {:error, {:unexpected_sequence, 0, bytes}} + end + end + + @doc ~S""" + Decodes the given JSON but raises an exception in case of errors. + + Returns the decoded content. See `decode/1` for possible errors. + + ## Examples + + iex> JSON.decode!("[null,123,\"string\",{\"key\":\"value\"}]") + [nil, 123, "string", %{"key" => "value"}] + """ + @spec decode!(binary()) :: term() + def decode!(binary) when is_binary(binary) do + case decode(binary) do + {:ok, decoded} -> + decoded + + {:error, {:unexpected_end, offset}} -> + raise JSON.DecodeError, + message: + "unexpected end of JSON binary at position " <> + "(byte offset) #{offset}", + data: binary, + offset: offset + + {:error, {:invalid_byte, offset, byte}} -> + raise JSON.DecodeError, + message: + "invalid byte #{byte} at position " <> + "(byte offset) #{offset}", + data: binary, + offset: offset + + {:error, {:unexpected_sequence, offset, bytes}} -> + raise JSON.DecodeError, + message: + "unexpected sequence #{inspect(bytes)} at position " <> + "(byte offset) #{offset}", + data: binary, + offset: offset + end + end + + @doc ~S""" + Encodes the given term to JSON as a binary. + + ## Examples + + iex> JSON.encode!([123, "string", %{key: "value"}]) + "[123,\"string\",{\"key\":\"value\"}]" + """ + @spec encode!(term(), (term(), (... -> iodata()) -> iodata())) :: + binary() + def encode!(term, encoder \\ &protocol_encode/2) do + IO.iodata_to_binary(encoder.(term, encoder)) + end + + @doc ~S""" + Encodes the given term to JSON as iodata. + + This is the most efficient format if the JSON is going to be + used for IO purposes. + + ## Examples + + iex> data = JSON.encode_to_iodata!([123, "string", %{key: "value"}]) + iex> IO.iodata_to_binary(data) + "[123,\"string\",{\"key\":\"value\"}]" + """ + @spec encode_to_iodata!( + term(), + (term(), (... -> iodata()) -> iodata()) + ) :: iodata() + def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do + encoder.(term, encoder) + end + + @doc """ + Default encode implementation passed to `encode!/1`. + + Optimized dispatch to the `JSON.Encoder` protocol. + """ + def protocol_encode(value, encoder) when is_atom(value) do + case value do + nil -> "null" + true -> "true" + false -> "false" + _ -> encoder.(Atom.to_string(value), encoder) + end + end + + def protocol_encode(value, _encoder) when is_binary(value), + do: :json.encode_binary(value) + + def protocol_encode(value, _encoder) when is_integer(value), + do: :json.encode_integer(value) + + def protocol_encode(value, _encoder) when is_float(value), + do: :json.encode_float(value) + + def protocol_encode(value, encoder) when is_list(value), + do: :json.encode_list(value, encoder) + + def protocol_encode(%{} = value, encoder) + when not is_map_key(value, :__struct__), + do: JSON.Encoder.Map.encode(value, encoder) + + def protocol_encode(value, encoder), + do: JSON.Encoder.encode(value, encoder) +end diff --git a/tests/libs/estdlib/CMakeLists.txt b/tests/libs/estdlib/CMakeLists.txt index 7b3aa2a191..b4b396a527 100644 --- a/tests/libs/estdlib/CMakeLists.txt +++ b/tests/libs/estdlib/CMakeLists.txt @@ -34,6 +34,7 @@ set(ERLANG_MODULES test_gen_tcp test_inet test_io_lib + test_json test_lists test_logger test_maps diff --git a/tests/libs/estdlib/test_json.erl b/tests/libs/estdlib/test_json.erl new file mode 100644 index 0000000000..c661310288 --- /dev/null +++ b/tests/libs/estdlib/test_json.erl @@ -0,0 +1,1567 @@ +% +% This file is part of AtomVM. +% +% Copyright 2016 Nicolas Seriot (JSONTestSuite test data, MIT license) +% Copyright 2026 Davide Bettio +% +% Generated by generate_test_data.escript +% Source: JSONTestSuite/test_parsing/ +% +% SPDX-License-Identifier: MIT +% +% n_structure_100000_opening_arrays.json and n_structure_open_array_object.json +% are tested as scaled-down equivalents (10K/5K depth) in test_deep_nesting/0. +% + +-module(test_json). + +-export([test/0]). + +-include("etest.hrl"). + +test() -> + ok = test_y(), + ok = test_n(), + ok = test_i(), + ok = test_t(), + ok = test_y_roundtrip(), + ok = test_i_roundtrip(), + ok = test_decode_basic(), + ok = test_decode_strings(), + ok = test_decode_containers(), + ok = test_decode_whitespace(), + ok = test_decode_errors(), + ok = test_decode3(), + ok = test_decode_streaming(), + ok = test_decode_weather(), + ok = test_decode_edge_cases(), + ok = test_encode_basic(), + ok = test_encode_strings(), + ok = test_encode_arrays(), + ok = test_encode_objects(), + ok = test_encode_key_value_list(), + ok = test_encode_checked(), + ok = test_encode_errors(), + ok = test_encode_custom_key_encoder(), + ok = test_encode_checked_custom_key_encoder(), + ok = test_roundtrip(), + ok = test_streaming_splits(), + ok = test_streaming_end_of_input(), + ok = test_streaming_number_boundary(), + ok = test_hex_error_classification(), + ok = test_streaming_sliding_split(), + ok = test_streaming_byte_by_byte(), + ok = test_deep_nesting(), + ok = test_encode_truncated_utf8(), + ok = test_encode_checked_normalizing_encoder(), + ok = test_encode_weather(), + ok. + +test_y() -> + lists:foreach(fun run_y_test/1, y_tests()), + ok. + +test_n() -> + lists:foreach(fun run_n_test/1, n_tests()), + ok. + +test_i() -> + lists:foreach(fun run_i_test/1, i_tests()), + ok. + +test_t() -> + lists:foreach(fun run_i_test/1, t_tests()), + ok. + +test_y_roundtrip() -> + lists:foreach(fun run_y_roundtrip/1, y_tests()), + ok. + +test_i_roundtrip() -> + lists:foreach(fun run_i_roundtrip/1, i_tests()), + ok. + +run_y_test({_Name, B64, Expected0}) -> + Expected = resolve(Expected0), + Expected = json:decode(base64:decode(B64)). + +run_n_test({Name, B64, _}) -> + Bin = base64:decode(B64), + try json:decode(Bin) of + Result -> error({should_have_failed, Name, Result}) + catch + error:_ -> ok + end. + +run_i_test({Name, B64, Expected0}) -> + case resolve(Expected0) of + {ok, Expected} -> + Expected = json:decode(base64:decode(B64)); + {error, _} -> + Bin = base64:decode(B64), + try json:decode(Bin) of + Result -> error({should_have_failed, Name, Result}) + catch + error:_ -> ok + end + end. + +run_y_roundtrip({_Name, B64, Expected0}) -> + Expected = resolve(Expected0), + Decoded = json:decode(base64:decode(B64)), + Decoded = json:decode(iolist_to_binary(json:encode(Expected))). + +run_i_roundtrip({_Name, B64, Expected0}) -> + case resolve(Expected0) of + {ok, Expected} -> + Decoded = json:decode(base64:decode(B64)), + Decoded = json:decode(iolist_to_binary(json:encode(Expected))); + {error, _} -> + ok + end. + +resolve({external_term, Enc}) -> + binary_to_term(base64:decode(Enc)); +resolve(Term) -> + Term. + +to_bin(IoData) -> iolist_to_binary(IoData). + +test_decode_basic() -> + 0 = json:decode(<<"0">>), + 1 = json:decode(<<"1">>), + 42 = json:decode(<<"42">>), + 123456789 = json:decode(<<"123456789">>), + -1 = json:decode(<<"-1">>), + -42 = json:decode(<<"-42">>), + + ?ASSERT_EQUALS(0.0, json:decode(<<"0.0">>)), + ?ASSERT_EQUALS(3.14, json:decode(<<"3.14">>)), + ?ASSERT_EQUALS(-2.5, json:decode(<<"-2.5">>)), + ?ASSERT_EQUALS(0.1, json:decode(<<"0.1">>)), + ?ASSERT_EQUALS(100.0, json:decode(<<"100.0">>)), + + ?ASSERT_EQUALS(1.0e5, json:decode(<<"1e5">>)), + ?ASSERT_EQUALS(1.0e5, json:decode(<<"1E5">>)), + ?ASSERT_EQUALS(1.0e5, json:decode(<<"1e+5">>)), + ?ASSERT_EQUALS(1.0e-3, json:decode(<<"1e-3">>)), + ?ASSERT_EQUALS(1.5e3, json:decode(<<"1.5e3">>)), + ?ASSERT_EQUALS(1.5e-2, json:decode(<<"1.5e-2">>)), + ?ASSERT_EQUALS(-1.0e5, json:decode(<<"-1e5">>)), + ?ASSERT_EQUALS(-2.5e10, json:decode(<<"-2.5e10">>)), + ?ASSERT_EQUALS(0.0e0, json:decode(<<"0e0">>)), + ?ASSERT_EQUALS(0.0e0, json:decode(<<"0E0">>)), + + 0 = json:decode(<<"-0">>), + ?ASSERT_EQUALS(-0.0, json:decode(<<"-0.0">>)), + + true = json:decode(<<"true">>), + false = json:decode(<<"false">>), + null = json:decode(<<"null">>), + ok. + +test_decode_strings() -> + <<>> = json:decode(<<"\"\"">>), + <<"hello">> = json:decode(<<"\"hello\"">>), + <<"hello world">> = json:decode(<<"\"hello world\"">>), + + <<"\"">> = json:decode(<<"\"\\\"\"">>), + <<"\\">> = json:decode(<<"\"\\\\\"">>), + <<"/">> = json:decode(<<"\"\\/\"">>), + <<"\b">> = json:decode(<<"\"\\b\"">>), + <<"\f">> = json:decode(<<"\"\\f\"">>), + <<"\n">> = json:decode(<<"\"\\n\"">>), + <<"\r">> = json:decode(<<"\"\\r\"">>), + <<"\t">> = json:decode(<<"\"\\t\"">>), + + <<"hello\nworld">> = json:decode(<<"\"hello\\nworld\"">>), + <<"\t\n\r">> = json:decode(<<"\"\\t\\n\\r\"">>), + + <<"A">> = json:decode(<<"\"\\u0041\"">>), + <<16#C3, 16#A9>> = json:decode(<<"\"\\u00e9\"">>), + <<16#C3, 16#A9>> = json:decode(<<"\"\\u00E9\"">>), + <<0>> = json:decode(<<"\"\\u0000\"">>), + <<16#E4, 16#B8, 16#96>> = json:decode(<<"\"\\u4e16\"">>), + + <<16#F0, 16#9F, 16#98, 16#80>> = json:decode(<<"\"\\uD83D\\uDE00\"">>), + + <<"A\nB">> = json:decode(<<"\"\\u0041\\nB\"">>), + ok. + +test_decode_containers() -> + [] = json:decode(<<"[]">>), + [1] = json:decode(<<"[1]">>), + [1, 2, 3] = json:decode(<<"[1,2,3]">>), + [1, <<"two">>, true, null] = json:decode(<<"[1, \"two\", true, null]">>), + [[]] = json:decode(<<"[[]]">>), + [[1, 2], [3, 4]] = json:decode(<<"[[1,2],[3,4]]">>), + [[[1]]] = json:decode(<<"[[[1]]]">>), + + ?ASSERT_MATCH(#{}, json:decode(<<"{}">>)), + ?ASSERT_MATCH(#{<<"a">> => 1}, json:decode(<<"{\"a\": 1}">>)), + ?ASSERT_MATCH(#{<<"a">> => 1, <<"b">> => 2}, json:decode(<<"{\"a\": 1, \"b\": 2}">>)), + ?ASSERT_MATCH(#{<<"a">> => #{<<"b">> => 1}}, json:decode(<<"{\"a\": {\"b\": 1}}">>)), + ?ASSERT_MATCH(#{<<"items">> => [1, 2, 3]}, json:decode(<<"{\"items\": [1, 2, 3]}">>)), + ok. + +test_decode_whitespace() -> + 42 = json:decode(<<" 42 ">>), + 42 = json:decode(<<"\t42\n">>), + 42 = json:decode(<<"\r\n42\r\n">>), + [1, 2] = json:decode(<<"[ 1 , 2 ]">>), + ?ASSERT_MATCH(#{<<"a">> => 1}, json:decode(<<"{ \"a\" : 1 }">>)), + [1] = json:decode(<<" \t\r\n[ \t\r\n1 \t\r\n] \t\r\n">>), + ok. + +test_decode_errors() -> + ?ASSERT_ERROR(json:decode(<<>>)), + ?ASSERT_ERROR(json:decode(<<" ">>)), + ?ASSERT_ERROR(json:decode(<<"\"unterminated">>)), + ?ASSERT_ERROR(json:decode(<<"[1,">>)), + ?ASSERT_ERROR(json:decode(<<"{\"a\"">>)), + ?ASSERT_ERROR(json:decode(<<"{\"a\":">>)), + ?ASSERT_ERROR(json:decode(<<"[">>)), + ?ASSERT_ERROR(json:decode(<<"{">>)), + + ?ASSERT_ERROR(json:decode(<<"x">>)), + ?ASSERT_ERROR(json:decode(<<",">>)), + ?ASSERT_ERROR(json:decode(<<"42x">>)), + ?ASSERT_ERROR(json:decode(<<"\"", 0, "\"">>)), + ?ASSERT_ERROR(json:decode(<<"\"hello\nworld\"">>)), + + ?ASSERT_ERROR(json:decode(<<"\"\\uD800\"">>)), + ?ASSERT_ERROR(json:decode(<<"\"\\uDC00\"">>)), + ?ASSERT_ERROR(json:decode(<<"\"\\uD800\\u0041\"">>)), + + ?ASSERT_ERROR(json:decode(<<"-">>)), + ?ASSERT_ERROR(json:decode(<<"1.">>)), + ?ASSERT_ERROR(json:decode(<<"1e">>)), + ?ASSERT_ERROR(json:decode(<<"1e+">>)), + ok. + +test_decode3() -> + {42, my_acc, <<>>} = json:decode(<<"42">>, my_acc, #{}), + {42, my_acc, <<"rest">>} = json:decode(<<"42rest">>, my_acc, #{}), + + {nil, ok, <<>>} = json:decode(<<"null">>, ok, #{null => nil}), + + StringFun = fun(Bin) -> binary_to_atom(Bin, utf8) end, + {hello, ok, <<>>} = json:decode(<<"\"hello\"">>, ok, #{string => StringFun}), + + IntFun = fun(Bin) -> binary_to_integer(Bin) * 2 end, + {84, ok, <<>>} = json:decode(<<"42">>, ok, #{integer => IntFun}), + + FloatFun = fun(Bin) -> round(binary_to_float(Bin)) end, + {3, ok, <<>>} = json:decode(<<"3.14">>, ok, #{float => FloatFun}), + + ArrayDecoders = #{ + array_start => fun(_) -> {[], 0} end, + array_push => fun(V, {Acc, Idx}) -> {[{Idx, V} | Acc], Idx + 1} end, + array_finish => fun({Acc, _Idx}, OldAcc) -> {lists:reverse(Acc), OldAcc} end + }, + {[{0, 1}, {1, 2}, {2, 3}], ok, <<>>} = json:decode(<<"[1,2,3]">>, ok, ArrayDecoders), + + ObjDecoders = #{ + object_start => fun(_) -> [] end, + object_push => fun(K, V, Acc) -> [{K, V} | Acc] end, + object_finish => fun(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc} end + }, + {[{<<"a">>, 1}, {<<"b">>, 2}], ok, <<>>} = + json:decode(<<"{\"a\":1,\"b\":2}">>, ok, ObjDecoders), + + {42, ok, <<>>} = json:decode(<<"42 ">>, ok, #{}), + {42, ok, <<" x">>} = json:decode(<<"42 x">>, ok, #{}), + {true, ok, <<>>} = json:decode(<<"true ">>, ok, #{}), + {true, ok, <<" x">>} = json:decode(<<"true x">>, ok, #{}), + ok. + +test_decode_streaming() -> + {true, ok, <<>>} = json:decode_start(<<"true">>, ok, #{}), + + {continue, S0} = json:decode_start(<<"42">>, ok, #{}), + {42, ok, <<>>} = json:decode_continue(end_of_input, S0), + + {continue, S1} = json:decode_start(<<"[1,2">>, ok, #{}), + {[1, 2, 3], ok, <<>>} = json:decode_continue(<<",3]">>, S1), + + {continue, S2} = json:decode_start(<<"\"hel">>, ok, #{}), + {<<"hello">>, ok, <<>>} = json:decode_continue(<<"lo\"">>, S2), + + {continue, S3} = json:decode_start(<<"{">>, ok, #{}), + {continue, S4} = json:decode_continue(<<"\"a\"">>, S3), + {continue, S5} = json:decode_continue(<<":">>, S4), + {continue, S6} = json:decode_continue(<<"1">>, S5), + ?ASSERT_MATCH(#{<<"a">> => 1}, element(1, json:decode_continue(<<"}">>, S6))), + + {continue, S7} = json:decode_start(<<"[1,">>, ok, #{}), + ?ASSERT_ERROR(json:decode_continue(end_of_input, S7)), + + {continue, S8} = json:decode_start(<<>>, ok, #{}), + {continue, S9} = json:decode_continue(<<"42">>, S8), + {42, ok, <<>>} = json:decode_continue(end_of_input, S9), + + {continue, S10} = json:decode_start(<<>>, ok, #{}), + {42, ok, <<" x">>} = json:decode_continue(<<"42 x">>, S10), + {continue, S11} = json:decode_start(<<>>, ok, #{}), + {42, ok, <<>>} = json:decode_continue(<<"42 ">>, S11), + + ?ASSERT_ERROR(json:decode_start(<<"x">>, ok, #{})), + ok. + +test_decode_weather() -> + Json = + << + "{" + "\"location\": {" + "\"name\": \"\xe9\x82\xa3\xe9\xa0\x88\\u753A\"," + "\"country\": \"JP\"," + "\"coordinates\": [140.1209, 37.0198]," + "\"utc_offset\": 9" + "}," + "\"current\": {" + "\"temperature\": 18.5," + "\"humidity\": 62," + "\"conditions\": [\"partly_cloudy\", \"windy\"]," + "\"wind\": {\"speed\": 5.2, \"direction\": \"NNW\"}," + "\"precipitation\": false" + "}," + "\"forecast\": [" + "{\"day\": 1, \"high\": 22.0, \"low\": 12.5, \"rain_probability\": 0.15}," + "{\"day\": 2, \"high\": 19.0, \"low\": 10.0, \"rain_probability\": 0.80}" + "]," + "\"alerts\": null" + "}" + >>, + Result = json:decode(Json), + + Location = maps:get(<<"location">>, Result), + <<16#E9, 16#82, 16#A3, 16#E9, 16#A0, 16#88, 16#E7, 16#94, 16#BA>> = maps:get( + <<"name">>, Location + ), + <<"JP">> = maps:get(<<"country">>, Location), + [140.1209, 37.0198] = maps:get(<<"coordinates">>, Location), + 9 = maps:get(<<"utc_offset">>, Location), + + Current = maps:get(<<"current">>, Result), + ?ASSERT_EQUALS(18.5, maps:get(<<"temperature">>, Current)), + 62 = maps:get(<<"humidity">>, Current), + [<<"partly_cloudy">>, <<"windy">>] = maps:get(<<"conditions">>, Current), + ?ASSERT_MATCH( + #{<<"speed">> => 5.2, <<"direction">> => <<"NNW">>}, + maps:get(<<"wind">>, Current) + ), + false = maps:get(<<"precipitation">>, Current), + + [Day1, Day2] = maps:get(<<"forecast">>, Result), + 1 = maps:get(<<"day">>, Day1), + ?ASSERT_EQUALS(22.0, maps:get(<<"high">>, Day1)), + ?ASSERT_EQUALS(0.80, maps:get(<<"rain_probability">>, Day2)), + + null = maps:get(<<"alerts">>, Result), + ok. + +test_decode_edge_cases() -> + 0 = json:decode(<<"0">>), + ?ASSERT_EQUALS(0.5, json:decode(<<"0.5">>)), + ?ASSERT_ERROR(json:decode(<<"01">>)), + {0, ok, <<"1">>} = json:decode(<<"01">>, ok, #{}), + + 9007199254740993 = json:decode(<<"9007199254740993">>), + + ?ASSERT_EQUALS(1.0, json:decode(<<"1.0e0">>)), + ?ASSERT_EQUALS(1.0, json:decode(<<"1.0e+0">>)), + ?ASSERT_EQUALS(1.0, json:decode(<<"1.0e-0">>)), + ok. + +test_encode_basic() -> + <<"0">> = to_bin(json:encode(0)), + <<"42">> = to_bin(json:encode(42)), + <<"-1">> = to_bin(json:encode(-1)), + <<"9007199254740993">> = to_bin(json:encode(9007199254740993)), + + <<"1.0">> = to_bin(json:encode(1.0)), + <<"3.14">> = to_bin(json:encode(3.14)), + <<"-2.5">> = to_bin(json:encode(-2.5)), + <<"-0.0">> = to_bin(json:encode(-0.0)), + + <<"true">> = to_bin(json:encode(true)), + <<"false">> = to_bin(json:encode(false)), + <<"null">> = to_bin(json:encode(null)), + + <<"\"hello\"">> = to_bin(json:encode(hello)), + ok. + +test_encode_strings() -> + <<"\"\"">> = to_bin(json:encode(<<>>)), + <<"\"hello\"">> = to_bin(json:encode(<<"hello">>)), + <<"\"hello world\"">> = to_bin(json:encode(<<"hello world">>)), + + <<"\"\\\\\"">> = to_bin(json:encode(<<"\\">>)), + <<"\"\\\"\"">> = to_bin(json:encode(<<"\"">>)), + <<"\"\\n\"">> = to_bin(json:encode(<<"\n">>)), + <<"\"\\r\"">> = to_bin(json:encode(<<"\r">>)), + <<"\"\\t\"">> = to_bin(json:encode(<<"\t">>)), + <<"\"\\b\"">> = to_bin(json:encode(<<"\b">>)), + <<"\"\\f\"">> = to_bin(json:encode(<<"\f">>)), + <<"\"\\u0001\"">> = to_bin(json:encode(<<1>>)), + + <<"\"hello\\nworld\"">> = to_bin(json:encode(<<"hello\nworld">>)), + + Bin = <<16#C3, 16#A9>>, + <<"\"", 16#C3, 16#A9, "\"">> = to_bin(json:encode(Bin)), + + <<"\"hello\"">> = to_bin(json:encode_binary_escape_all(<<"hello">>)), + <<"\"\\u00E9\"">> = to_bin(json:encode_binary_escape_all(<<16#C3, 16#A9>>)), + <<"\"\\uD83D\\uDE00\"">> = to_bin( + json:encode_binary_escape_all(<<16#F0, 16#9F, 16#98, 16#80>>) + ), + + ?ASSERT_ERROR(json:encode(<<16#FF>>)), + ?ASSERT_ERROR(json:encode(<<16#80>>)), + ok. + +test_encode_arrays() -> + <<"[]">> = to_bin(json:encode([])), + <<"[1]">> = to_bin(json:encode([1])), + <<"[1,2,3]">> = to_bin(json:encode([1, 2, 3])), + <<"[1,\"two\",true,null]">> = to_bin(json:encode([1, <<"two">>, true, null])), + <<"[[1,2],[3,4]]">> = to_bin(json:encode([[1, 2], [3, 4]])), + + ?ASSERT_EQUALS( + <<"{\"hello\":[119,111,114,108,100]}">>, + to_bin(json:encode(#{hello => "world"})) + ), + ok. + +test_encode_objects() -> + <<"{}">> = to_bin(json:encode(#{})), + + ?ASSERT_MATCH( + #{<<"a">> => 1, <<"b">> => 2}, + json:decode(to_bin(json:encode(#{<<"a">> => 1, <<"b">> => 2}))) + ), + + AtomResult = json:decode(to_bin(json:encode(#{foo => 1, bar => 2}))), + 1 = maps:get(<<"foo">>, AtomResult), + 2 = maps:get(<<"bar">>, AtomResult), + + ?ASSERT_EQUALS( + to_bin(json:encode(#{<<"hello">> => <<"world">>})), + to_bin(json:encode(#{hello => <<"world">>})) + ), + + IntResult = json:decode(to_bin(json:encode(#{1 => <<"one">>, 2 => <<"two">>}))), + <<"one">> = maps:get(<<"1">>, IntResult), + <<"two">> = maps:get(<<"2">>, IntResult), + + Nested = #{<<"a">> => #{<<"b">> => 1}}, + ?ASSERT_MATCH(Nested, json:decode(to_bin(json:encode(Nested)))), + ok. + +test_encode_key_value_list() -> + ?ASSERT_EQUALS( + <<"{\"a\":1,\"b\":2}">>, + to_bin(json:encode_key_value_list([{<<"a">>, 1}, {<<"b">>, 2}], fun json:encode_value/2)) + ), + ok. + +test_encode_checked() -> + ?ASSERT_ERROR( + json:encode_key_value_list_checked( + [{<<"a">>, 1}, {<<"a">>, 2}], fun json:encode_value/2 + ) + ), + + ?ASSERT_ERROR( + json:encode_map_checked(#{foo => 1, <<"foo">> => 2}, fun json:encode_value/2) + ), + ok. + +test_encode_errors() -> + ?ASSERT_ERROR(json:encode({a, tuple})), + ?ASSERT_ERROR(json:encode([{hello, <<"world">>}])), + ?ASSERT_ERROR(json:encode(#{"hello" => <<"world">>})), + ok. + +test_roundtrip() -> + Terms = [ + 42, + -1, + 0, + 3.14, + -0.0, + true, + false, + null, + <<"hello">>, + <<>>, + [], + [1, 2, 3], + #{}, + #{<<"key">> => <<"value">>}, + [#{<<"a">> => [1, true, null]}] + ], + lists:foreach( + fun(Term) -> + ?ASSERT_EQUALS(Term, json:decode(to_bin(json:encode(Term)))) + end, + Terms + ), + ok. + +test_streaming_splits() -> + {continue, S5} = json:decode_start(<<"tr">>, ok, #{}), + {true, ok, <<>>} = json:decode_continue(<<"ue">>, S5), + + {continue, S6} = json:decode_start(<<"nu">>, ok, #{}), + {null, ok, <<>>} = json:decode_continue(<<"ll">>, S6), + + {continue, S7} = json:decode_start(<<"\"hel">>, ok, #{}), + {<<"hello">>, ok, <<>>} = json:decode_continue(<<"lo\"">>, S7), + + {continue, S8} = json:decode_start(<<"\"\\u00">>, ok, #{}), + {<<"A">>, ok, <<>>} = json:decode_continue(<<"41\"">>, S8), + + {continue, S9} = json:decode_start(<<"[1,">>, ok, #{}), + {[1, 2], ok, <<>>} = json:decode_continue(<<"2]">>, S9), + + {continue, S10} = json:decode_start(<<"-">>, ok, #{}), + {-42, ok, <<>>} = json:decode_continue(<<"42 ">>, S10), + + {continue, S3} = json:decode_start(<<"3.">>, ok, #{}), + {3.14, ok, <<>>} = json:decode_continue(<<"14 ">>, S3), + + {continue, S4} = json:decode_start(<<"1e">>, ok, #{}), + {1.0e5, ok, <<>>} = json:decode_continue(<<"5 ">>, S4), + + {continue, S11} = json:decode_start(<<"1.5e">>, ok, #{}), + {1.5e3, ok, <<>>} = json:decode_continue(<<"3 ">>, S11), + ok. + +test_streaming_end_of_input() -> + {continue, S1} = json:decode_start(<<"[1,">>, ok, #{}), + ok = + try json:decode_continue(end_of_input, S1) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + + {continue, S2} = json:decode_start(<<>>, ok, #{}), + ok = + try json:decode_continue(end_of_input, S2) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + + {continue, S3} = json:decode_start(<<"\"abc">>, ok, #{}), + ok = + try json:decode_continue(end_of_input, S3) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + + {continue, S4} = json:decode_start(<<"tru">>, ok, #{}), + ok = + try json:decode_continue(end_of_input, S4) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + ok. + +test_streaming_number_boundary() -> + {continue, S1} = json:decode_start(<<"42">>, ok, #{}), + {continue, S2} = json:decode_continue(<<"3">>, S1), + {423, ok, <<>>} = json:decode_continue(end_of_input, S2), + + {continue, S3} = json:decode_start(<<"1">>, ok, #{}), + {12, ok, <<>>} = json:decode_continue(<<"2 ">>, S3), + + {continue, S4} = json:decode_start(<<"0">>, ok, #{}), + {0, ok, <<>>} = json:decode_continue(end_of_input, S4), + + {continue, S5} = json:decode_start(<<"3.14">>, ok, #{}), + {3.14, ok, <<>>} = json:decode_continue(end_of_input, S5), + + {continue, S6} = json:decode_start(<<"1e5">>, ok, #{}), + {1.0e5, ok, <<>>} = json:decode_continue(end_of_input, S6), + + {continue, S7} = json:decode_start(<<"-42">>, ok, #{}), + {-42, ok, <<>>} = json:decode_continue(end_of_input, S7), + + 42 = json:decode(<<"42">>), + 0 = json:decode(<<"0">>), + 3.14 = json:decode(<<"3.14">>), + 1.0e5 = json:decode(<<"1e5">>), + -1 = json:decode(<<"-1">>), + ok. + +test_hex_error_classification() -> + ok = + try json:decode(<<"\"\\uGGGG\"">>) of + _ -> error(should_have_failed) + catch + error:{unexpected_sequence, _} -> ok; + error:{invalid_byte, _} -> error(wrong_error_tag) + end, + + ok = + try json:decode(<<"\"\\uD83D\\uZZZZ\"">>) of + _ -> error(should_have_failed) + catch + error:{unexpected_sequence, _} -> ok; + error:{invalid_byte, _} -> error(wrong_error_tag) + end, + ok. + +test_encode_custom_key_encoder() -> + Enc = fun + (B, _E) when is_binary(B) -> json:encode_binary_escape_all(B); + (Other, E) -> json:encode_value(Other, E) + end, + Result = iolist_to_binary(json:encode_map(#{<<16#C3, 16#A9>> => <<"v">>}, Enc)), + Expected = <<"{\"\\u00E9\":\"v\"}">>, + ?ASSERT_EQUALS(Expected, Result), + ok. + +test_encode_checked_custom_key_encoder() -> + Enc = fun + (B, _E) when is_binary(B) -> json:encode_binary_escape_all(B); + (Other, E) -> json:encode_value(Other, E) + end, + Result = iolist_to_binary( + json:encode_map_checked(#{<<16#C3, 16#A9>> => <<"v">>}, Enc) + ), + Expected = <<"{\"\\u00E9\":\"v\"}">>, + ?ASSERT_EQUALS(Expected, Result), + ok. + +test_streaming_sliding_split() -> + Jsons = sliding_split_jsons(), + lists:foreach(fun run_sliding_split/1, Jsons), + ok. + +run_sliding_split(Json) -> + Expected = json:decode(Json), + Size = byte_size(Json), + lists:foreach( + fun(Pos) -> + Left = binary_part(Json, 0, Pos), + Right = binary_part(Json, Pos, Size - Pos), + Result = streaming_decode(Left, Right), + case Result of + Expected -> + ok; + Other -> + error( + {sliding_split_mismatch, [ + {json, Json}, + {split_at, Pos}, + {expected, Expected}, + {got, Other} + ]} + ) + end + end, + lists:seq(0, Size) + ). + +streaming_decode(Left, Right) -> + case json:decode_start(Left, ok, #{}) of + {Value, ok, Rest} -> + %% Value complete in first chunk — remaining must be whitespace only + case skip_ws(<>) of + <<>> -> Value; + _ -> error({unexpected_trailing, Left, Right, Value, Rest}) + end; + {continue, State} -> + streaming_continue(Right, State) + end. + +streaming_continue(<<>>, State) -> + {Value, ok, <<>>} = json:decode_continue(end_of_input, State), + Value; +streaming_continue(Data, State) -> + case json:decode_continue(Data, State) of + {Value, ok, _Rest} -> + Value; + {continue, State2} -> + {Value, ok, <<>>} = + json:decode_continue(end_of_input, State2), + Value + end. + +skip_ws(<<$\s, R/binary>>) -> skip_ws(R); +skip_ws(<<$\t, R/binary>>) -> skip_ws(R); +skip_ws(<<$\n, R/binary>>) -> skip_ws(R); +skip_ws(<<$\r, R/binary>>) -> skip_ws(R); +skip_ws(Bin) -> Bin. + +test_streaming_byte_by_byte() -> + lists:foreach(fun run_byte_by_byte/1, sliding_split_jsons()), + ok. + +run_byte_by_byte(Json) -> + Expected = json:decode(Json), + <> = Json, + {continue, State} = json:decode_start(<>, ok, #{}), + Result = feed_bytes(Rest, State), + case Result of + Expected -> ok; + Other -> error({byte_by_byte_mismatch, [{json, Json}, {expected, Expected}, {got, Other}]}) + end. + +feed_bytes(<<>>, State) -> + {Value, ok, <<>>} = json:decode_continue(end_of_input, State), + Value; +feed_bytes(<>, State) -> + case json:decode_continue(<>, State) of + {Value, ok, _} when Rest =:= <<>> -> Value; + {continue, State2} -> feed_bytes(Rest, State2) + end. + +sliding_split_jsons() -> + [ + %% Mixed types with null + <<"{\"temp\":18.5,\"wind\":null,\"rain\":false}">>, + %% Array of mixed types + <<"[1,-42,3.14,true,false,null,\"hello\"]">>, + %% Nested objects + <<"{\"a\":{\"b\":{\"c\":[1,2,3]}}}">>, + %% Unicode escapes + << + "{\"key\":\"caf\\u00E9\"," + "\"emoji\":\"\\uD83D\\uDE00\"}" + >>, + %% Kanji via unicode escapes + << + "{\"name\":\"\\u90A3\\u9808\\u753A\"," + "\"country\":\"JP\"}" + >>, + %% Array of objects + << + "[{\"id\":1,\"v\":true}," + "{\"id\":2,\"v\":false}]" + >>, + %% Floats and exponents + <<"{\"data\":[1.0e5,-0.0,1.5e-3],\"ok\":true}">>, + %% String escapes + << + "{\"path\":\"C:\\\\dir\\\\file\"," + "\"msg\":\"say \\\"hi\\\"\\n\"}" + >>, + %% Large integer + empty containers + << + "{\"big\":9007199254740993," + "\"a\":[],\"o\":{}}" + >>, + %% Complex nested + << + "[{\"k\":[null,[true,{\"x\":-1}]]," + "\"y\":\"z\"}]" + >>, + %% Long UTF-8 (U+FBFD, 3-byte) + << + "{\"longutf8\":\"\xEF\xAF\xB9\"," + "\"codepoint\":64505}" + >>, + %% Repeated long UTF-8 + << + "{\"longutf8repeated\":" + "\"\xEF\xAF\xB9\xEF\xAF\xB9\xEF\xAF\xB9" + "\xEF\xAF\xB9\xEF\xAF\xB9\"," + "\"is_repeated\":true}" + >>, + + <<"42">>, + <<"-1">>, + <<"0">>, + <<"3.14">>, + <<"-0.5">>, + <<"1e5">>, + <<"1.5e-3">>, + <<"-2.5e10">>, + <<"true">>, + <<"false">>, + <<"null">>, + <<"\"hello\"">>, + <<"\"\"">>, + <<"\"caf\\u00E9\"">>, + <<"\"\\uD83D\\uDE00\"">>, + <<"\"", 230, 188, 162, 229, 173, 151, "\"">>, + <<"\"\\uD83D\\uDE00", 231, 181, 181, 230, 150, 135, 229, 173, 151, "\"">>, + <<"\"nottrue\"">>, + <<"\"notfalse\"">>, + <<"\"notnull\"">>, + <<"\"not42\"">>, + <<"\"true-as-suffix\"">>, + <<"\"false-as-suffix\"">>, + <<"\"null-as-suffix\"">>, + <<"\"true\"">>, + <<"\"false\"">>, + <<"\"null\"">>, + + << + "{\"location\":{\"name\":\"\\u90A3\\u9808\\u753A\"," + "\"country\":\"JP\"," + "\"coordinates\":[140.1209,37.0198]," + "\"utc_offset\":9}," + "\"current\":{\"temperature\":18.5," + "\"humidity\":62," + "\"conditions\":[\"partly_cloudy\",\"windy\"]," + "\"wind\":{\"speed\":5.2,\"direction\":\"NNW\"}," + "\"precipitation\":false}," + "\"forecast\":[" + "{\"day\":1,\"high\":22.0,\"low\":12.5," + "\"rain_probability\":0.15}," + "{\"day\":2,\"high\":19.0,\"low\":10.0," + "\"rain_probability\":0.80}]," + "\"alerts\":null}" + >> + ]. + +test_deep_nesting() -> + %% 1000 nested arrays: [[[...[1]...]]] + N = 1000, + Open = binary:copy(<<"[">>, N), + Close = binary:copy(<<"]">>, N), + Bin = <>, + Expected = nest_value(1, N), + Expected = json:decode(Bin), + %% 1000 nested objects: {"k":{"k":...1...}} + ObjOpen = iolist_to_binary(lists:duplicate(N, <<"{\"k\":">>)), + ObjClose = binary:copy(<<"}">>, N), + ObjBin = <>, + _ = json:decode(ObjBin), + {continue, S1} = json:decode_start(Open, ok, #{}), + {Expected, ok, <<>>} = json:decode_continue(<<"1", Close/binary>>, S1), + ok = + try json:decode(Open) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + %% Equivalent to the JSONTestSuite files that were excluded: + %% n_structure_100000_opening_arrays.json (100000 x "[") + %% n_structure_open_array_object.json (50000 x "[{"":") + %% Full-size (100K) would pass but takes too long due to AtomVM GC pressure + %% on the growing stack list. 10K proves the iterative decoder handles + %% arbitrary depth without call-stack overflow. + ok = expect_error(binary:copy(<<"[">>, 10000)), + ok = expect_error(binary:copy(<<"[{\"\":">>, 5000)), + ok. + +expect_error(Bin) -> + try json:decode(Bin) of + _ -> error(should_have_failed) + catch + error:_ -> ok + end. + +nest_value(Value, 0) -> Value; +nest_value(Value, N) -> nest_value([Value], N - 1). + +%% +%% JSONTestSuite data +%% + +y_tests() -> + [ + {"y_array_arraysWithSpaces.json", <<"W1tdICAgXQ==">>, [[]]}, + {"y_array_empty-string.json", <<"WyIiXQ==">>, [<<>>]}, + {"y_array_empty.json", <<"W10=">>, []}, + {"y_array_ending_with_newline.json", <<"WyJhIl0=">>, [<<97>>]}, + {"y_array_false.json", <<"W2ZhbHNlXQ==">>, [false]}, + {"y_array_heterogeneous.json", <<"W251bGwsIDEsICIxIiwge31d">>, [null, 1, <<49>>, #{}]}, + {"y_array_null.json", <<"W251bGxd">>, [null]}, + {"y_array_with_1_and_newline.json", <<"WzEKXQ==">>, [1]}, + {"y_array_with_leading_space.json", <<"IFsxXQ==">>, [1]}, + {"y_array_with_several_null.json", <<"WzEsbnVsbCxudWxsLG51bGwsMl0=">>, [ + 1, null, null, null, 2 + ]}, + {"y_array_with_trailing_space.json", <<"WzJdIA==">>, [2]}, + {"y_number.json", <<"WzEyM2U2NV0=">>, [1.23e67]}, + {"y_number_0e+1.json", <<"WzBlKzFd">>, [0.0]}, + {"y_number_0e1.json", <<"WzBlMV0=">>, [0.0]}, + {"y_number_after_space.json", <<"WyA0XQ==">>, [4]}, + {"y_number_double_close_to_zero.json", + <<"Wy0wLjAwMDAwMDAwMDAwMDAwMDAwMDAw", "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", "MDAwMDAwMDAwMV0K">>, + [-1.0e-78]}, + {"y_number_int_with_exp.json", <<"WzIwZTFd">>, [200.0]}, + {"y_number_minus_zero.json", <<"Wy0wXQ==">>, [0]}, + {"y_number_negative_int.json", <<"Wy0xMjNd">>, [-123]}, + {"y_number_negative_one.json", <<"Wy0xXQ==">>, [-1]}, + {"y_number_negative_zero.json", <<"Wy0wXQ==">>, [0]}, + {"y_number_real_capital_e.json", <<"WzFFMjJd">>, [1.0e22]}, + {"y_number_real_capital_e_neg_exp.json", <<"WzFFLTJd">>, [0.01]}, + {"y_number_real_capital_e_pos_exp.json", <<"WzFFKzJd">>, [100.0]}, + {"y_number_real_exponent.json", <<"WzEyM2U0NV0=">>, [1.23e47]}, + {"y_number_real_fraction_exponent.json", <<"WzEyMy40NTZlNzhd">>, [1.23456e80]}, + {"y_number_real_neg_exp.json", <<"WzFlLTJd">>, [0.01]}, + {"y_number_real_pos_exponent.json", <<"WzFlKzJd">>, [100.0]}, + {"y_number_simple_int.json", <<"WzEyM10=">>, [123]}, + {"y_number_simple_real.json", <<"WzEyMy40NTY3ODld">>, [123.456789]}, + {"y_object.json", <<"eyJhc2QiOiJzZGYiLCAiZGZnIjoiZmdo", "In0=">>, + {external_term, <<"g3QAAAACbQAAAANhc2RtAAAAA3NkZm0A", "AAADZGZnbQAAAANmZ2g=">>}}, + {"y_object_basic.json", <<"eyJhc2QiOiJzZGYifQ==">>, #{ + <<97, 115, 100>> => <<115, 100, 102>> + }}, + {"y_object_duplicated_key.json", <<"eyJhIjoiYiIsImEiOiJjIn0=">>, #{<<97>> => <<98>>}}, + {"y_object_duplicated_key_and_value.json", <<"eyJhIjoiYiIsImEiOiJiIn0=">>, #{ + <<97>> => <<98>> + }}, + {"y_object_empty.json", <<"e30=">>, #{}}, + {"y_object_empty_key.json", <<"eyIiOjB9">>, #{<<>> => 0}}, + {"y_object_escaped_null_in_key.json", <<"eyJmb29cdTAwMDBiYXIiOiA0Mn0=">>, #{ + <<102, 111, 111, 0, 98, 97, 114>> => 42 + }}, + {"y_object_extreme_numbers.json", + <<"eyAibWluIjogLTEuMGUrMjgsICJtYXgi", "OiAxLjBlKzI4IH0=">>, #{ + <<109, 97, 120>> => 1.0e28, <<109, 105, 110>> => -1.0e28 + }}, + {"y_object_long_strings.json", + <<"eyJ4IjpbeyJpZCI6ICJ4eHh4eHh4eHh4", "eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4", + "eHh4eHh4In1dLCAiaWQiOiAieHh4eHh4", "eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4", + "eHh4eHh4eHh4eCJ9">>, + {external_term, + <<"g3QAAAACbQAAAAJpZG0AAAAoeHh4eHh4", "eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4", + "eHh4eHh4eHh4eG0AAAABeGwAAAABdAAA", "AAFtAAAAAmlkbQAAACh4eHh4eHh4eHh4", + "eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4", "eHh4eHh4ag==">>}}, + {"y_object_simple.json", <<"eyJhIjpbXX0=">>, #{<<97>> => []}}, + {"y_object_string_unicode.json", + <<"eyJ0aXRsZSI6Ilx1MDQxZlx1MDQzZVx1", "MDQzYlx1MDQ0Mlx1MDQzZVx1MDQ0MFx1", + "MDQzMCBcdTA0MTdcdTA0MzVcdTA0M2Nc", "dTA0M2JcdTA0MzVcdTA0M2FcdTA0M2Vc", + "dTA0M2ZcdTA0MzAiIH0=">>, + {external_term, + <<"g3QAAAABbQAAAAV0aXRsZW0AAAAh0J/Q", "vtC70YLQvtGA0LAg0JfQtdC80LvQtdC6", + "0L7Qv9Cw">>}}, + {"y_object_with_newlines.json", <<"ewoiYSI6ICJiIgp9">>, #{<<97>> => <<98>>}}, + {"y_string_1_2_3_bytes_UTF-8_sequences.json", <<"WyJcdTAwNjBcdTAxMmFcdTEyQUIiXQ==">>, [ + <<96, 196, 170, 225, 138, 171>> + ]}, + {"y_string_accepted_surrogate_pair.json", <<"WyJcdUQ4MDFcdWRjMzciXQ==">>, [ + <<240, 144, 144, 183>> + ]}, + {"y_string_accepted_surrogate_pairs.json", + <<"WyJcdWQ4M2RcdWRlMzlcdWQ4M2RcdWRj", "OGQiXQ==">>, [ + <<240, 159, 152, 185, 240, 159, 146, 141>> + ]}, + {"y_string_allowed_escapes.json", <<"WyJcIlxcXC9cYlxmXG5cclx0Il0=">>, [ + <<34, 92, 47, 8, 12, 10, 13, 9>> + ]}, + {"y_string_backslash_and_u_escaped_zero.json", <<"WyJcXHUwMDAwIl0=">>, [ + <<92, 117, 48, 48, 48, 48>> + ]}, + {"y_string_backslash_doublequotes.json", <<"WyJcIiJd">>, [<<34>>]}, + {"y_string_comments.json", <<"WyJhLypiKi9jLypkLy9lIl0=">>, [ + <<97, 47, 42, 98, 42, 47, 99, 47, 42, 100, 47, 47, 101>> + ]}, + {"y_string_double_escape_a.json", <<"WyJcXGEiXQ==">>, [<<92, 97>>]}, + {"y_string_double_escape_n.json", <<"WyJcXG4iXQ==">>, [<<92, 110>>]}, + {"y_string_escaped_control_character.json", <<"WyJcdTAwMTIiXQ==">>, [<<18>>]}, + {"y_string_escaped_noncharacter.json", <<"WyJcdUZGRkYiXQ==">>, [<<239, 191, 191>>]}, + {"y_string_in_array.json", <<"WyJhc2QiXQ==">>, [<<97, 115, 100>>]}, + {"y_string_in_array_with_leading_space.json", <<"WyAiYXNkIl0=">>, [<<97, 115, 100>>]}, + {"y_string_last_surrogates_1_and_2.json", <<"WyJcdURCRkZcdURGRkYiXQ==">>, [ + <<244, 143, 191, 191>> + ]}, + {"y_string_nbsp_uescaped.json", <<"WyJuZXdcdTAwQTBsaW5lIl0=">>, [ + <<110, 101, 119, 194, 160, 108, 105, 110, 101>> + ]}, + {"y_string_nonCharacterInUTF-8_U+10FFFF.json", <<"WyL0j7+/Il0=">>, [<<244, 143, 191, 191>>]}, + {"y_string_nonCharacterInUTF-8_U+FFFF.json", <<"WyLvv78iXQ==">>, [<<239, 191, 191>>]}, + {"y_string_null_escape.json", <<"WyJcdTAwMDAiXQ==">>, [<<0>>]}, + {"y_string_one-byte-utf-8.json", <<"WyJcdTAwMmMiXQ==">>, [<<44>>]}, + {"y_string_pi.json", <<"WyLPgCJd">>, [<<207, 128>>]}, + {"y_string_reservedCharacterInUTF-8_U+1BFFF.json", <<"WyLwm7+/Il0=">>, [ + <<240, 155, 191, 191>> + ]}, + {"y_string_simple_ascii.json", <<"WyJhc2QgIl0=">>, [<<97, 115, 100, 32>>]}, + {"y_string_space.json", <<"IiAi">>, <<32>>}, + {"y_string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json", <<"WyJcdUQ4MzRcdURkMWUiXQ==">>, [ + <<240, 157, 132, 158>> + ]}, + {"y_string_three-byte-utf-8.json", <<"WyJcdTA4MjEiXQ==">>, [<<224, 160, 161>>]}, + {"y_string_two-byte-utf-8.json", <<"WyJcdTAxMjMiXQ==">>, [<<196, 163>>]}, + {"y_string_u+2028_line_sep.json", <<"WyLigKgiXQ==">>, [<<226, 128, 168>>]}, + {"y_string_u+2029_par_sep.json", <<"WyLigKkiXQ==">>, [<<226, 128, 169>>]}, + {"y_string_uEscape.json", <<"WyJcdTAwNjFcdTMwYWZcdTMwRUFcdTMw", "YjkiXQ==">>, [ + <<97, 227, 130, 175, 227, 131, 170, 227, 130, 185>> + ]}, + {"y_string_uescaped_newline.json", <<"WyJuZXdcdTAwMEFsaW5lIl0=">>, [ + <<110, 101, 119, 10, 108, 105, 110, 101>> + ]}, + {"y_string_unescaped_char_delete.json", <<"WyJ/Il0=">>, [<<127>>]}, + {"y_string_unicode.json", <<"WyJcdUE2NkQiXQ==">>, [<<234, 153, 173>>]}, + {"y_string_unicodeEscapedBackslash.json", <<"WyJcdTAwNUMiXQ==">>, [<<92>>]}, + {"y_string_unicode_2.json", <<"WyLijYLjiLTijYIiXQ==">>, [ + <<226, 141, 130, 227, 136, 180, 226, 141, 130>> + ]}, + {"y_string_unicode_U+10FFFE_nonchar.json", <<"WyJcdURCRkZcdURGRkUiXQ==">>, [ + <<244, 143, 191, 190>> + ]}, + {"y_string_unicode_U+1FFFE_nonchar.json", <<"WyJcdUQ4M0ZcdURGRkUiXQ==">>, [ + <<240, 159, 191, 190>> + ]}, + {"y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json", <<"WyJcdTIwMEIiXQ==">>, [ + <<226, 128, 139>> + ]}, + {"y_string_unicode_U+2064_invisible_plus.json", <<"WyJcdTIwNjQiXQ==">>, [<<226, 129, 164>>]}, + {"y_string_unicode_U+FDD0_nonchar.json", <<"WyJcdUZERDAiXQ==">>, [<<239, 183, 144>>]}, + {"y_string_unicode_U+FFFE_nonchar.json", <<"WyJcdUZGRkUiXQ==">>, [<<239, 191, 190>>]}, + {"y_string_unicode_escaped_double_quote.json", <<"WyJcdTAwMjIiXQ==">>, [<<34>>]}, + {"y_string_utf8.json", <<"WyLigqzwnYSeIl0=">>, [<<226, 130, 172, 240, 157, 132, 158>>]}, + {"y_string_with_del_character.json", <<"WyJhf2EiXQ==">>, [<<97, 127, 97>>]}, + {"y_structure_lonely_false.json", <<"ZmFsc2U=">>, false}, + {"y_structure_lonely_int.json", <<"NDI=">>, 42}, + {"y_structure_lonely_negative_real.json", <<"LTAuMQ==">>, -0.1}, + {"y_structure_lonely_null.json", <<"bnVsbA==">>, null}, + {"y_structure_lonely_string.json", <<"ImFzZCI=">>, <<97, 115, 100>>}, + {"y_structure_lonely_true.json", <<"dHJ1ZQ==">>, true}, + {"y_structure_string_empty.json", <<"IiI=">>, <<>>}, + {"y_structure_trailing_newline.json", <<"WyJhIl0K">>, [<<97>>]}, + {"y_structure_true_in_array.json", <<"W3RydWVd">>, [true]}, + {"y_structure_whitespace_array.json", <<"IFtdIA==">>, []} + ]. + +n_tests() -> + [ + {"n_array_1_true_without_comma.json", <<"WzEgdHJ1ZV0=">>, {invalid_byte, 116}}, + {"n_array_a_invalid_utf8.json", <<"W2HlXQ==">>, {invalid_byte, 97}}, + {"n_array_colon_instead_of_comma.json", <<"WyIiOiAxXQ==">>, {invalid_byte, 58}}, + {"n_array_comma_after_close.json", <<"WyIiXSw=">>, {invalid_byte, 44}}, + {"n_array_comma_and_number.json", <<"WywxXQ==">>, {invalid_byte, 44}}, + {"n_array_double_comma.json", <<"WzEsLDJd">>, {invalid_byte, 44}}, + {"n_array_double_extra_comma.json", <<"WyJ4IiwsXQ==">>, {invalid_byte, 44}}, + {"n_array_extra_close.json", <<"WyJ4Il1d">>, {invalid_byte, 93}}, + {"n_array_extra_comma.json", <<"WyIiLF0=">>, {invalid_byte, 93}}, + {"n_array_incomplete.json", <<"WyJ4Ig==">>, unexpected_end}, + {"n_array_incomplete_invalid_value.json", <<"W3g=">>, {invalid_byte, 120}}, + {"n_array_inner_array_no_comma.json", <<"WzNbNF1d">>, {invalid_byte, 91}}, + {"n_array_invalid_utf8.json", <<"W/9d">>, {invalid_byte, 255}}, + {"n_array_items_separated_by_semicolon.json", <<"WzE6Ml0=">>, {invalid_byte, 58}}, + {"n_array_just_comma.json", <<"Wyxd">>, {invalid_byte, 44}}, + {"n_array_just_minus.json", <<"Wy1d">>, {invalid_byte, 93}}, + {"n_array_missing_value.json", <<"WyAgICwgIiJd">>, {invalid_byte, 44}}, + {"n_array_newlines_unclosed.json", <<"WyJhIiwKNAosMSw=">>, unexpected_end}, + {"n_array_number_and_comma.json", <<"WzEsXQ==">>, {invalid_byte, 93}}, + {"n_array_number_and_several_commas.json", <<"WzEsLF0=">>, {invalid_byte, 44}}, + {"n_array_spaces_vertical_tab_formfeed.json", <<"WyILYSJcZl0=">>, {invalid_byte, 11}}, + {"n_array_star_inside.json", <<"Wypd">>, {invalid_byte, 42}}, + {"n_array_unclosed.json", <<"WyIi">>, unexpected_end}, + {"n_array_unclosed_trailing_comma.json", <<"WzEs">>, unexpected_end}, + {"n_array_unclosed_with_new_lines.json", <<"WzEsCjEKLDE=">>, unexpected_end}, + {"n_array_unclosed_with_object_inside.json", <<"W3t9">>, unexpected_end}, + {"n_incomplete_false.json", <<"W2ZhbHNd">>, unexpected_end}, + {"n_incomplete_null.json", <<"W251bF0=">>, unexpected_end}, + {"n_incomplete_true.json", <<"W3RydV0=">>, unexpected_end}, + {"n_multidigit_number_then_00.json", <<"MTIzAA==">>, {invalid_byte, 0}}, + {"n_number_++.json", <<"WysrMTIzNF0=">>, {invalid_byte, 43}}, + {"n_number_+1.json", <<"WysxXQ==">>, {invalid_byte, 43}}, + {"n_number_+Inf.json", <<"WytJbmZd">>, {invalid_byte, 43}}, + {"n_number_-01.json", <<"Wy0wMV0=">>, {invalid_byte, 49}}, + {"n_number_-1.0..json", <<"Wy0xLjAuXQ==">>, {invalid_byte, 46}}, + {"n_number_-2..json", <<"Wy0yLl0=">>, {invalid_byte, 93}}, + {"n_number_-NaN.json", <<"Wy1OYU5d">>, {invalid_byte, 78}}, + {"n_number_.-1.json", <<"Wy4tMV0=">>, {invalid_byte, 46}}, + {"n_number_.2e-3.json", <<"Wy4yZS0zXQ==">>, {invalid_byte, 46}}, + {"n_number_0.1.2.json", <<"WzAuMS4yXQ==">>, {invalid_byte, 46}}, + {"n_number_0.3e+.json", <<"WzAuM2UrXQ==">>, {invalid_byte, 93}}, + {"n_number_0.3e.json", <<"WzAuM2Vd">>, {invalid_byte, 93}}, + {"n_number_0.e1.json", <<"WzAuZTFd">>, {invalid_byte, 101}}, + {"n_number_0_capital_E+.json", <<"WzBFK10=">>, {invalid_byte, 93}}, + {"n_number_0_capital_E.json", <<"WzBFXQ==">>, {invalid_byte, 93}}, + {"n_number_0e+.json", <<"WzBlK10=">>, {invalid_byte, 93}}, + {"n_number_0e.json", <<"WzBlXQ==">>, {invalid_byte, 93}}, + {"n_number_1.0e+.json", <<"WzEuMGUrXQ==">>, {invalid_byte, 93}}, + {"n_number_1.0e-.json", <<"WzEuMGUtXQ==">>, {invalid_byte, 93}}, + {"n_number_1.0e.json", <<"WzEuMGVd">>, {invalid_byte, 93}}, + {"n_number_1_000.json", <<"WzEgMDAwLjBd">>, {invalid_byte, 48}}, + {"n_number_1eE2.json", <<"WzFlRTJd">>, {invalid_byte, 69}}, + {"n_number_2.e+3.json", <<"WzIuZSszXQ==">>, {invalid_byte, 101}}, + {"n_number_2.e-3.json", <<"WzIuZS0zXQ==">>, {invalid_byte, 101}}, + {"n_number_2.e3.json", <<"WzIuZTNd">>, {invalid_byte, 101}}, + {"n_number_9.e+.json", <<"WzkuZStd">>, {invalid_byte, 101}}, + {"n_number_Inf.json", <<"W0luZl0=">>, {invalid_byte, 73}}, + {"n_number_NaN.json", <<"W05hTl0=">>, {invalid_byte, 78}}, + {"n_number_U+FF11_fullwidth_digit_one.json", <<"W++8kV0=">>, {invalid_byte, 239}}, + {"n_number_expression.json", <<"WzErMl0=">>, {invalid_byte, 43}}, + {"n_number_hex_1_digit.json", <<"WzB4MV0=">>, {invalid_byte, 120}}, + {"n_number_hex_2_digits.json", <<"WzB4NDJd">>, {invalid_byte, 120}}, + {"n_number_infinity.json", <<"W0luZmluaXR5XQ==">>, {invalid_byte, 73}}, + {"n_number_invalid+-.json", <<"WzBlKy0xXQ==">>, {invalid_byte, 45}}, + {"n_number_invalid-negative-real.json", <<"Wy0xMjMuMTIzZm9vXQ==">>, {invalid_byte, 102}}, + {"n_number_invalid-utf-8-in-bigger-int.json", <<"WzEyM+Vd">>, {invalid_byte, 229}}, + {"n_number_invalid-utf-8-in-exponent.json", <<"WzFlMeVd">>, {invalid_byte, 229}}, + {"n_number_invalid-utf-8-in-int.json", <<"WzDlXQo=">>, {invalid_byte, 229}}, + {"n_number_minus_infinity.json", <<"Wy1JbmZpbml0eV0=">>, {invalid_byte, 73}}, + {"n_number_minus_sign_with_trailing_garbage.json", <<"Wy1mb29d">>, {invalid_byte, 102}}, + {"n_number_minus_space_1.json", <<"Wy0gMV0=">>, {invalid_byte, 32}}, + {"n_number_neg_int_starting_with_zero.json", <<"Wy0wMTJd">>, {invalid_byte, 49}}, + {"n_number_neg_real_without_int_part.json", <<"Wy0uMTIzXQ==">>, {invalid_byte, 46}}, + {"n_number_neg_with_garbage_at_end.json", <<"Wy0xeF0=">>, {invalid_byte, 120}}, + {"n_number_real_garbage_after_e.json", <<"WzFlYV0=">>, {invalid_byte, 97}}, + {"n_number_real_with_invalid_utf8_after_e.json", <<"WzFl5V0=">>, {invalid_byte, 229}}, + {"n_number_real_without_fractional_part.json", <<"WzEuXQ==">>, {invalid_byte, 93}}, + {"n_number_starting_with_dot.json", <<"Wy4xMjNd">>, {invalid_byte, 46}}, + {"n_number_with_alpha.json", <<"WzEuMmEtM10=">>, {invalid_byte, 97}}, + {"n_number_with_alpha_char.json", <<"WzEuODAxMTY3MDAzMzM3NjUxNEgtMzA4", "XQ==">>, + {invalid_byte, 72}}, + {"n_number_with_leading_zero.json", <<"WzAxMl0=">>, {invalid_byte, 49}}, + {"n_object_bad_value.json", <<"WyJ4IiwgdHJ1dGhd">>, {invalid_byte, 114}}, + {"n_object_bracket_key.json", <<"e1s6ICJ4In0K">>, {invalid_byte, 91}}, + {"n_object_comma_instead_of_colon.json", <<"eyJ4IiwgbnVsbH0=">>, {invalid_byte, 44}}, + {"n_object_double_colon.json", <<"eyJ4Ijo6ImIifQ==">>, {invalid_byte, 58}}, + {"n_object_emoji.json", <<"e/Cfh6jwn4etfQ==">>, {invalid_byte, 240}}, + {"n_object_garbage_at_end.json", <<"eyJhIjoiYSIgMTIzfQ==">>, {invalid_byte, 49}}, + {"n_object_key_with_single_quotes.json", <<"e2tleTogJ3ZhbHVlJ30=">>, {invalid_byte, 107}}, + {"n_object_lone_continuation_byte_in_key_and_trailing_comma.json", <<"eyK5IjoiMCIsfQ==">>, + {invalid_byte, 185}}, + {"n_object_missing_colon.json", <<"eyJhIiBifQ==">>, {invalid_byte, 98}}, + {"n_object_missing_key.json", <<"ezoiYiJ9">>, {invalid_byte, 58}}, + {"n_object_missing_semicolon.json", <<"eyJhIiAiYiJ9">>, {invalid_byte, 34}}, + {"n_object_missing_value.json", <<"eyJhIjo=">>, unexpected_end}, + {"n_object_no-colon.json", <<"eyJhIg==">>, unexpected_end}, + {"n_object_non_string_key.json", <<"ezE6MX0=">>, {invalid_byte, 49}}, + {"n_object_non_string_key_but_huge_number_instead.json", <<"ezk5OTlFOTk5OToxfQ==">>, + {invalid_byte, 57}}, + {"n_object_repeated_null_null.json", <<"e251bGw6bnVsbCxudWxsOm51bGx9">>, + {invalid_byte, 110}}, + {"n_object_several_trailing_commas.json", <<"eyJpZCI6MCwsLCwsfQ==">>, {invalid_byte, 44}}, + {"n_object_single_quote.json", <<"eydhJzowfQ==">>, {invalid_byte, 39}}, + {"n_object_trailing_comma.json", <<"eyJpZCI6MCx9">>, {invalid_byte, 125}}, + {"n_object_trailing_comment.json", <<"eyJhIjoiYiJ9LyoqLw==">>, {invalid_byte, 47}}, + {"n_object_trailing_comment_open.json", <<"eyJhIjoiYiJ9LyoqLy8=">>, {invalid_byte, 47}}, + {"n_object_trailing_comment_slash_open.json", <<"eyJhIjoiYiJ9Ly8=">>, {invalid_byte, 47}}, + {"n_object_trailing_comment_slash_open_incomplete.json", <<"eyJhIjoiYiJ9Lw==">>, + {invalid_byte, 47}}, + {"n_object_two_commas_in_a_row.json", <<"eyJhIjoiYiIsLCJjIjoiZCJ9">>, {invalid_byte, 44}}, + {"n_object_unquoted_key.json", <<"e2E6ICJiIn0=">>, {invalid_byte, 97}}, + {"n_object_unterminated-value.json", <<"eyJhIjoiYQ==">>, unexpected_end}, + {"n_object_with_single_string.json", <<"eyAiZm9vIiA6ICJiYXIiLCAiYSIgfQ==">>, + {invalid_byte, 125}}, + {"n_object_with_trailing_garbage.json", <<"eyJhIjoiYiJ9Iw==">>, {invalid_byte, 35}}, + {"n_single_space.json", <<"IA==">>, unexpected_end}, + {"n_string_1_surrogate_then_escape.json", <<"WyJcdUQ4MDBcIl0=">>, unexpected_end}, + {"n_string_1_surrogate_then_escape_u.json", <<"WyJcdUQ4MDBcdSJd">>, unexpected_end}, + {"n_string_1_surrogate_then_escape_u1.json", <<"WyJcdUQ4MDBcdTEiXQ==">>, unexpected_end}, + {"n_string_1_surrogate_then_escape_u1x.json", <<"WyJcdUQ4MDBcdTF4Il0=">>, + {external_term, <<"g2gCdxN1bmV4cGVjdGVkX3NlcXVlbmNl", "bQAAAAxcdUQ4MDBcdTF4Il0=">>}}, + {"n_string_accentuated_char_no_quotes.json", <<"W8OpXQ==">>, {invalid_byte, 195}}, + {"n_string_backslash_00.json", <<"WyJcACJd">>, {invalid_byte, 0}}, + {"n_string_escape_x.json", <<"WyJceDAwIl0=">>, {invalid_byte, 120}}, + {"n_string_escaped_backslash_bad.json", <<"WyJcXFwiXQ==">>, unexpected_end}, + {"n_string_escaped_ctrl_char_tab.json", <<"WyJcCSJd">>, {invalid_byte, 9}}, + {"n_string_escaped_emoji.json", <<"WyJc8J+MgCJd">>, {invalid_byte, 240}}, + {"n_string_incomplete_escape.json", <<"WyJcIl0=">>, unexpected_end}, + {"n_string_incomplete_escaped_character.json", <<"WyJcdTAwQSJd">>, + {unexpected_sequence, <<92, 117, 48, 48, 65, 34>>}}, + {"n_string_incomplete_surrogate.json", <<"WyJcdUQ4MzRcdURkIl0=">>, + {external_term, <<"g2gCdxN1bmV4cGVjdGVkX3NlcXVlbmNl", "bQAAAAxcdUQ4MzRcdURkIl0=">>}}, + {"n_string_incomplete_surrogate_escape_invalid.json", <<"WyJcdUQ4MDBcdUQ4MDBceCJd">>, + {unexpected_sequence, <<92, 117, 68, 56, 48, 48, 92, 117, 68, 56, 48, 48>>}}, + {"n_string_invalid-utf-8-in-escape.json", <<"WyJcdeUiXQ==">>, unexpected_end}, + {"n_string_invalid_backslash_esc.json", <<"WyJcYSJd">>, {invalid_byte, 97}}, + {"n_string_invalid_unicode_escape.json", <<"WyJcdXFxcXEiXQ==">>, + {unexpected_sequence, <<92, 117, 113, 113, 113, 113>>}}, + {"n_string_invalid_utf8_after_escape.json", <<"WyJc5SJd">>, {invalid_byte, 229}}, + {"n_string_leading_uescaped_thinspace.json", <<"W1x1MDAyMCJhc2QiXQ==">>, + {invalid_byte, 92}}, + {"n_string_no_quotes_with_bad_escape.json", <<"W1xuXQ==">>, {invalid_byte, 92}}, + {"n_string_single_doublequote.json", <<"Ig==">>, unexpected_end}, + {"n_string_single_quote.json", <<"WydzaW5nbGUgcXVvdGUnXQ==">>, {invalid_byte, 39}}, + {"n_string_single_string_no_double_quotes.json", <<"YWJj">>, {invalid_byte, 97}}, + {"n_string_start_escape_unclosed.json", <<"WyJc">>, unexpected_end}, + {"n_string_unescaped_ctrl_char.json", <<"WyJhAGEiXQ==">>, {invalid_byte, 0}}, + {"n_string_unescaped_newline.json", <<"WyJuZXcKbGluZSJd">>, {invalid_byte, 10}}, + {"n_string_unescaped_tab.json", <<"WyIJIl0=">>, {invalid_byte, 9}}, + {"n_string_unicode_CapitalU.json", <<"IlxVQTY2RCI=">>, {invalid_byte, 85}}, + {"n_string_with_trailing_garbage.json", <<"IiJ4">>, {invalid_byte, 120}}, + {"n_structure_U+2060_word_joined.json", <<"W+KBoF0=">>, {invalid_byte, 226}}, + {"n_structure_UTF8_BOM_no_data.json", <<"77u/">>, {invalid_byte, 239}}, + {"n_structure_angle_bracket_..json", <<"PC4+">>, {invalid_byte, 60}}, + {"n_structure_angle_bracket_null.json", <<"WzxudWxsPl0=">>, {invalid_byte, 60}}, + {"n_structure_array_trailing_garbage.json", <<"WzFdeA==">>, {invalid_byte, 120}}, + {"n_structure_array_with_extra_array_close.json", <<"WzFdXQ==">>, {invalid_byte, 93}}, + {"n_structure_array_with_unclosed_string.json", <<"WyJhc2Rd">>, unexpected_end}, + {"n_structure_ascii-unicode-identifier.json", <<"YcOl">>, {invalid_byte, 97}}, + {"n_structure_capitalized_True.json", <<"W1RydWVd">>, {invalid_byte, 84}}, + {"n_structure_close_unopened_array.json", <<"MV0=">>, {invalid_byte, 93}}, + {"n_structure_comma_instead_of_closing_brace.json", <<"eyJ4IjogdHJ1ZSw=">>, unexpected_end}, + {"n_structure_double_array.json", <<"W11bXQ==">>, {invalid_byte, 91}}, + {"n_structure_end_array.json", <<"XQ==">>, {invalid_byte, 93}}, + {"n_structure_incomplete_UTF8_BOM.json", <<"77t7fQ==">>, {invalid_byte, 239}}, + {"n_structure_lone-invalid-utf-8.json", <<"5Q==">>, {invalid_byte, 229}}, + {"n_structure_lone-open-bracket.json", <<"Ww==">>, unexpected_end}, + {"n_structure_no_data.json", <<"">>, unexpected_end}, + {"n_structure_null-byte-outside-string.json", <<"WwBd">>, {invalid_byte, 0}}, + {"n_structure_number_with_trailing_garbage.json", <<"MkA=">>, {invalid_byte, 64}}, + {"n_structure_object_followed_by_closing_object.json", <<"e319">>, {invalid_byte, 125}}, + {"n_structure_object_unclosed_no_value.json", <<"eyIiOg==">>, unexpected_end}, + {"n_structure_object_with_comment.json", <<"eyJhIjovKmNvbW1lbnQqLyJiIn0=">>, + {invalid_byte, 47}}, + {"n_structure_object_with_trailing_garbage.json", <<"eyJhIjogdHJ1ZX0gIngi">>, + {invalid_byte, 32}}, + {"n_structure_open_array_apostrophe.json", <<"Wyc=">>, {invalid_byte, 39}}, + {"n_structure_open_array_comma.json", <<"Wyw=">>, {invalid_byte, 44}}, + {"n_structure_open_array_open_object.json", <<"W3s=">>, unexpected_end}, + {"n_structure_open_array_open_string.json", <<"WyJh">>, unexpected_end}, + {"n_structure_open_array_string.json", <<"WyJhIg==">>, unexpected_end}, + {"n_structure_open_object.json", <<"ew==">>, unexpected_end}, + {"n_structure_open_object_close_array.json", <<"e10=">>, {invalid_byte, 93}}, + {"n_structure_open_object_comma.json", <<"eyw=">>, {invalid_byte, 44}}, + {"n_structure_open_object_open_array.json", <<"e1s=">>, {invalid_byte, 91}}, + {"n_structure_open_object_open_string.json", <<"eyJh">>, unexpected_end}, + {"n_structure_open_object_string_with_apostrophes.json", <<"eydhJw==">>, + {invalid_byte, 39}}, + {"n_structure_open_open.json", <<"WyJce1siXHtbIlx7WyJcew==">>, {invalid_byte, 123}}, + {"n_structure_single_eacute.json", <<"6Q==">>, {invalid_byte, 233}}, + {"n_structure_single_star.json", <<"Kg==">>, {invalid_byte, 42}}, + {"n_structure_trailing_#.json", <<"eyJhIjoiYiJ9I3t9">>, {invalid_byte, 35}}, + {"n_structure_uescaped_LF_before_string.json", <<"W1x1MDAwQSIiXQ==">>, {invalid_byte, 92}}, + {"n_structure_unclosed_array.json", <<"WzE=">>, unexpected_end}, + {"n_structure_unclosed_array_partial_null.json", <<"WyBmYWxzZSwgbnVs">>, unexpected_end}, + {"n_structure_unclosed_array_unfinished_false.json", <<"WyB0cnVlLCBmYWxz">>, + unexpected_end}, + {"n_structure_unclosed_array_unfinished_true.json", <<"WyBmYWxzZSwgdHJ1">>, unexpected_end}, + {"n_structure_unclosed_object.json", <<"eyJhc2QiOiJhc2Qi">>, unexpected_end}, + {"n_structure_unicode-identifier.json", <<"w6U=">>, {invalid_byte, 195}}, + {"n_structure_whitespace_U+2060_word_joiner.json", <<"W+KBoF0=">>, {invalid_byte, 226}}, + {"n_structure_whitespace_formfeed.json", <<"Wwxd">>, {invalid_byte, 12}} + ]. + +i_tests() -> + [ + {"i_number_double_huge_neg_exp.json", <<"WzEyMy40NTZlLTc4OV0=">>, {ok, [0.0]}}, + {"i_number_huge_exp.json", + <<"WzAuNGUwMDY2OTk5OTk5OTk5OTk5OTk5", "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", + "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", + "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", "OTk5OTk2OTk5OTk5OTAwNl0=">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAAIcwLjRlMDA2Njk5", + "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", + "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", "OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5", + "OTk5OTk5OTk5OTk5OTk5OTk5Njk5OTk5", "OTkwMDY=">>}}, + {"i_number_neg_int_huge_exp.json", <<"Wy0xZSs5OTk5XQ==">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAAAotMS4wZSs5OTk5">>}}, + {"i_number_pos_double_huge_exp.json", <<"WzEuNWUrOTk5OV0=">>, + {error, {unexpected_sequence, <<49, 46, 53, 101, 43, 57, 57, 57, 57>>}}}, + {"i_number_real_neg_overflow.json", <<"Wy0xMjMxMjNlMTAwMDAwXQ==">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAABAtMTIzMTIzLjBl", + "MTAwMDAw">>}}, + {"i_number_real_pos_overflow.json", <<"WzEyMzEyM2UxMDAwMDBd">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAAA8xMjMxMjMuMGUx", + "MDAwMDA=">>}}, + {"i_number_real_underflow.json", <<"WzEyM2UtMTAwMDAwMDBd">>, {ok, [0.0]}}, + {"i_number_too_big_neg_int.json", <<"Wy0xMjMxMjMxMjMxMjMxMjMxMjMxMjMx", "MjMxMjMxMjNd">>, + {ok, [-123123123123123123123123123123]}}, + {"i_number_too_big_pos_int.json", <<"WzEwMDAwMDAwMDAwMDAwMDAwMDAwMF0=">>, + {ok, [100000000000000000000]}}, + {"i_number_very_big_negative_int.json", + <<"Wy0yMzc0NjIzNzQ2NzMyNzY4OTQyNzk4", "MzI3NDk4MzI0MjM0Nzk4MjMyNDYzMjc4", "NDZd">>, + {ok, [-237462374673276894279832749832423479823246327846]}}, + {"i_object_key_lone_2nd_surrogate.json", <<"eyJcdURGQUEiOjB9">>, + {error, {unexpected_sequence, <<92, 117, 68, 70, 65, 65>>}}}, + {"i_string_1st_surrogate_but_2nd_missing.json", <<"WyJcdURBREEiXQ==">>, + {error, unexpected_end}}, + {"i_string_1st_valid_surrogate_2nd_invalid.json", <<"WyJcdUQ4ODhcdTEyMzQiXQ==">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAAAxcdUQ4ODhcdTEy", "MzQ=">>}}, + {"i_string_UTF-16LE_with_BOM.json", <<"//5bACIA6QAiAF0A">>, {error, {invalid_byte, 255}}}, + {"i_string_UTF-8_invalid_sequence.json", <<"WyLml6XRiPoiXQ==">>, + {error, {invalid_byte, 250}}}, + {"i_string_UTF8_surrogate_U+D800.json", <<"WyLtoIAiXQ==">>, {error, {invalid_byte, 160}}}, + {"i_string_incomplete_surrogate_and_escape_valid.json", <<"WyJcdUQ4MDBcbiJd">>, + {error, unexpected_end}}, + {"i_string_incomplete_surrogate_pair.json", <<"WyJcdURkMWVhIl0=">>, + {error, {unexpected_sequence, <<92, 117, 68, 100, 49, 101>>}}}, + {"i_string_incomplete_surrogates_escape_valid.json", <<"WyJcdUQ4MDBcdUQ4MDBcbiJd">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAAAxcdUQ4MDBcdUQ4", "MDA=">>}}, + {"i_string_invalid_lonely_surrogate.json", <<"WyJcdWQ4MDAiXQ==">>, {error, unexpected_end}}, + {"i_string_invalid_surrogate.json", <<"WyJcdWQ4MDBhYmMiXQ==">>, {error, unexpected_end}}, + {"i_string_invalid_utf-8.json", <<"WyL/Il0=">>, {error, {invalid_byte, 255}}}, + {"i_string_inverted_surrogates_U+1D11E.json", <<"WyJcdURkMWVcdUQ4MzQiXQ==">>, + {error, {unexpected_sequence, <<92, 117, 68, 100, 49, 101>>}}}, + {"i_string_iso_latin_1.json", <<"WyLpIl0=">>, {error, {invalid_byte, 34}}}, + {"i_string_lone_second_surrogate.json", <<"WyJcdURGQUEiXQ==">>, + {error, {unexpected_sequence, <<92, 117, 68, 70, 65, 65>>}}}, + {"i_string_lone_utf8_continuation_byte.json", <<"WyKBIl0=">>, {error, {invalid_byte, 129}}}, + {"i_string_not_in_unicode_range.json", <<"WyL0v7+/Il0=">>, {error, {invalid_byte, 191}}}, + {"i_string_overlong_sequence_2_bytes.json", <<"WyLAryJd">>, {error, {invalid_byte, 192}}}, + {"i_string_overlong_sequence_6_bytes.json", <<"WyL8g7+/v78iXQ==">>, + {error, {invalid_byte, 252}}}, + {"i_string_overlong_sequence_6_bytes_null.json", <<"WyL8gICAgIAiXQ==">>, + {error, {invalid_byte, 252}}}, + {"i_string_truncated-utf-8.json", <<"WyLg/yJd">>, {error, {invalid_byte, 255}}}, + {"i_string_utf16BE_no_BOM.json", <<"AFsAIgDpACIAXQ==">>, {error, {invalid_byte, 0}}}, + {"i_string_utf16LE_no_BOM.json", <<"WwAiAOkAIgBdAA==">>, {error, {invalid_byte, 0}}}, + {"i_structure_500_nested_arrays.json", + <<"W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", "W1tbW1tbW1tbW1tbW1tbW1tbW1tbW1tb", + "W1tbW1tbW1tbW1tbW1tbW1tbW1tdXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", + "XV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1d", "XV1dXV1dXV1dXV1dXV1dXQ==">>, + {external_term, + <<"g2gCdwJva2wAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", + "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", + "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", "AWwAAAABbAAAAAFsAAAAAWwAAAABbAAA", + "AAFsAAAAAWwAAAABbAAAAAFsAAAAAWwA", "AAABbAAAAAFsAAAAAWwAAAABbAAAAAFs", + "AAAAAWwAAAABbAAAAAFsAAAAAWwAAAAB", "bAAAAAFsAAAAAWwAAAABbAAAAAFsAAAA", + "AWwAAAABampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "ampqampqampqampqampqampqampqampq", + "ampqampqampqampqampqampqampqampq", "amo=">>}}, + {"i_structure_UTF-8_BOM_empty_object.json", <<"77u/e30=">>, {error, {invalid_byte, 239}}} + ]. + +t_tests() -> + [ + {"number_-9223372036854775808.json", <<"Wy05MjIzMzcyMDM2ODU0Nzc1ODA4XQo=">>, + {ok, [-9223372036854775808]}}, + {"number_-9223372036854775809.json", <<"Wy05MjIzMzcyMDM2ODU0Nzc1ODA5XQo=">>, + {ok, [-9223372036854775809]}}, + {"number_1.0.json", <<"WzEuMF0K">>, {ok, [1.0]}}, + {"number_1.000000000000000005.json", <<"WzEuMDAwMDAwMDAwMDAwMDAwMDA1XQo=">>, {ok, [1.0]}}, + {"number_1000000000000000.json", <<"WzEwMDAwMDAwMDAwMDAwMDBdCg==">>, + {ok, [1000000000000000]}}, + {"number_10000000000000000999.json", <<"WzEwMDAwMDAwMDAwMDAwMDAwOTk5XQo=">>, + {ok, [10000000000000000999]}}, + {"number_1e-999.json", <<"WzFFLTk5OV0K">>, {ok, [0.0]}}, + {"number_1e6.json", <<"WzFFNl0K">>, {ok, [1.0e6]}}, + {"number_9223372036854775807.json", <<"WzkyMjMzNzIwMzY4NTQ3NzU4MDddCg==">>, + {ok, [9223372036854775807]}}, + {"number_9223372036854775808.json", <<"WzkyMjMzNzIwMzY4NTQ3NzU4MDhdCg==">>, + {ok, [9223372036854775808]}}, + {"object_key_nfc_nfd.json", <<"eyLDqSI6Ik5GQyIsImXMgSI6Ik5GRCJ9">>, + {external_term, <<"g2gCdwJva3QAAAACbQAAAANlzIFtAAAA", "A05GRG0AAAACw6ltAAAAA05GQw==">>}}, + {"object_key_nfd_nfc.json", <<"eyJlzIEiOiJORkQiLCLDqSI6Ik5GQyJ9">>, + {external_term, <<"g2gCdwJva3QAAAACbQAAAANlzIFtAAAA", "A05GRG0AAAACw6ltAAAAA05GQw==">>}}, + {"object_same_key_different_values.json", <<"eyJhIjoxLCJhIjoyfQ==">>, {ok, #{<<97>> => 1}}}, + {"object_same_key_same_value.json", <<"eyJhIjoxLCJhIjoxfQ==">>, {ok, #{<<97>> => 1}}}, + {"object_same_key_unclear_values.json", <<"eyJhIjowLCAiYSI6LTB9Cg==">>, + {ok, #{<<97>> => 0}}}, + {"string_1_escaped_invalid_codepoint.json", <<"WyJcdUQ4MDAiXQ==">>, + {error, unexpected_end}}, + {"string_1_invalid_codepoint.json", <<"WyLtoIAiXQ==">>, {error, {invalid_byte, 160}}}, + {"string_2_escaped_invalid_codepoints.json", <<"WyJcdUQ4MDBcdUQ4MDAiXQ==">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAAAxcdUQ4MDBcdUQ4", "MDA=">>}}, + {"string_2_invalid_codepoints.json", <<"WyLtoIDtoIAiXQ==">>, {error, {invalid_byte, 160}}}, + {"string_3_escaped_invalid_codepoints.json", <<"WyJcdUQ4MDBcdUQ4MDBcdUQ4MDAiXQ==">>, + {external_term, + <<"g2gCdwVlcnJvcmgCdxN1bmV4cGVjdGVk", "X3NlcXVlbmNlbQAAAAxcdUQ4MDBcdUQ4", "MDA=">>}}, + {"string_3_invalid_codepoints.json", <<"WyLtoIDtoIDtoIAiXQ==">>, + {error, {invalid_byte, 160}}}, + {"string_with_escaped_NULL.json", <<"WyJBXHUwMDAwQiJd">>, {ok, [<<65, 0, 66>>]}} + ]. + +test_encode_truncated_utf8() -> + %% Valid truncated UTF-8 sequences must raise unexpected_end (not invalid_byte) + %% 2-byte lead, no continuation (e.g. start of é = C3 A9) + ok = + try json:encode_binary(<<16#C3>>) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + %% 3-byte lead alone (e.g. start of – = E2 80 93) + ok = + try json:encode_binary(<<16#E2>>) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + %% 3-byte lead + 1 continuation (valid prefix) + ok = + try json:encode_binary(<<16#E2, 16#80>>) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + %% 4-byte lead alone (e.g. start of U+1F600) + ok = + try json:encode_binary(<<16#F0>>) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + %% 4-byte lead + 1 continuation + ok = + try json:encode_binary(<<16#F0, 16#9F>>) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + %% 4-byte lead + 2 continuations + ok = + try json:encode_binary(<<16#F0, 16#9F, 16#98>>) of + _ -> error(should_have_failed) + catch + error:unexpected_end -> ok + end, + %% Truly invalid bytes must still raise {invalid_byte, _} + ok = + try json:encode_binary(<<16#FF>>) of + _ -> error(should_have_failed) + catch + error:{invalid_byte, 16#FF} -> ok + end, + ok = + try json:encode_binary(<<16#80>>) of + _ -> error(should_have_failed) + catch + error:{invalid_byte, 16#80} -> ok + end, + ok. + +test_encode_checked_normalizing_encoder() -> + %% Custom encoder that uppercases keys — duplicate detection must use + %% encoded (post-normalization) keys, not raw keys. + Enc = fun + (B, _E) when is_binary(B) -> + json:encode_binary(string:uppercase(B)); + (Other, E) -> + json:encode_value(Other, E) + end, + ?ASSERT_ERROR( + json:encode_map_checked(#{<<"foo">> => 1, <<"FOO">> => 2}, Enc) + ), + ?ASSERT_ERROR( + json:encode_key_value_list_checked( + [{<<"foo">>, 1}, {<<"FOO">>, 2}], Enc + ) + ), + ok. + +test_encode_weather() -> + NasuMachi = <<16#E9, 16#82, 16#A3, 16#E9, 16#A0, 16#88, 16#E7, 16#94, 16#BA>>, + Weather = #{ + <<"location">> => #{ + <<"name">> => NasuMachi, + <<"country">> => <<"JP">>, + <<"coordinates">> => [140.1209, 37.0198], + <<"utc_offset">> => 9 + }, + <<"current">> => #{ + <<"temperature">> => 18.5, + <<"humidity">> => 62, + <<"conditions">> => [<<"partly_cloudy">>, <<"windy">>], + <<"wind">> => #{<<"speed">> => 5.2, <<"direction">> => <<"NNW">>}, + <<"precipitation">> => false + }, + <<"forecast">> => [ + #{ + <<"day">> => 1, + <<"high">> => 22.0, + <<"low">> => 12.5, + <<"rain_probability">> => 0.15 + }, + #{<<"day">> => 2, <<"high">> => 19.0, <<"low">> => 10.0, <<"rain_probability">> => 0.80} + ], + <<"alerts">> => null + }, + Encoded = to_bin(json:encode(Weather)), + Decoded = json:decode(Encoded), + + Location = maps:get(<<"location">>, Decoded), + NasuMachi = maps:get(<<"name">>, Location), + <<"JP">> = maps:get(<<"country">>, Location), + [140.1209, 37.0198] = maps:get(<<"coordinates">>, Location), + 9 = maps:get(<<"utc_offset">>, Location), + + Current = maps:get(<<"current">>, Decoded), + ?ASSERT_EQUALS(18.5, maps:get(<<"temperature">>, Current)), + 62 = maps:get(<<"humidity">>, Current), + [<<"partly_cloudy">>, <<"windy">>] = maps:get(<<"conditions">>, Current), + ?ASSERT_EQUALS(5.2, maps:get(<<"speed">>, maps:get(<<"wind">>, Current))), + <<"NNW">> = maps:get(<<"direction">>, maps:get(<<"wind">>, Current)), + false = maps:get(<<"precipitation">>, Current), + + [Day1, Day2] = maps:get(<<"forecast">>, Decoded), + 1 = maps:get(<<"day">>, Day1), + ?ASSERT_EQUALS(22.0, maps:get(<<"high">>, Day1)), + ?ASSERT_EQUALS(12.5, maps:get(<<"low">>, Day1)), + ?ASSERT_EQUALS(0.15, maps:get(<<"rain_probability">>, Day1)), + 2 = maps:get(<<"day">>, Day2), + ?ASSERT_EQUALS(0.80, maps:get(<<"rain_probability">>, Day2)), + + null = maps:get(<<"alerts">>, Decoded), + ok. diff --git a/tests/libs/estdlib/tests.erl b/tests/libs/estdlib/tests.erl index b48d567ace..1cdd8f9043 100644 --- a/tests/libs/estdlib/tests.erl +++ b/tests/libs/estdlib/tests.erl @@ -55,8 +55,9 @@ get_otp_version() -> end. % test_sets heavily relies on is_equal that is from OTP-27 +% test_json requires json module that is from OTP-27 get_non_networking_tests(OTPVersion) when OTPVersion >= 27 -> - [test_sets | get_non_networking_tests(26)]; + [test_sets, test_json | get_non_networking_tests(26)]; get_non_networking_tests(_OTPVersion) -> [ test_apply, diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 26d34a6486..e2c2036dd2 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -35,6 +35,7 @@ defmodule Tests do :ok = test_chars_protocol() :ok = test_inspect() :ok = test_base() + :ok = test_json() :ok = test_gen_server() :ok = test_supervisor() :ok = IO.puts("Finished Elixir tests") @@ -559,4 +560,90 @@ defmodule Tests do :ok end + + defp test_json() do + {:ok, nil} = JSON.decode("null") + {:ok, true} = JSON.decode("true") + {:ok, false} = JSON.decode("false") + {:ok, 42} = JSON.decode("42") + {:ok, 3.14} = JSON.decode("3.14") + {:ok, "hello"} = JSON.decode(~s("hello")) + {:ok, [1, 2, 3]} = JSON.decode("[1,2,3]") + {:ok, %{"a" => 1}} = JSON.decode(~s({"a":1})) + + {:ok, [nil, true, false]} = JSON.decode("[null, true, false]") + + {:ok, %{"coord" => %{"lat" => 37.0198}}} = + JSON.decode(~s({"coord": {"lat": 37.0198}})) + + {:error, {:unexpected_end, _}} = JSON.decode("") + {:error, {:invalid_byte, _, _}} = JSON.decode("x") + {:error, {:invalid_byte, _, _}} = JSON.decode("[1] extra") + + {nil, :ok, ""} = JSON.decode("null", :ok, []) + {:none, :ok, ""} = JSON.decode("null", :ok, null: :none) + + decoders = [ + object_start: fn _ -> [] end, + object_push: fn k, v, acc -> [{k, v} | acc] end, + object_finish: fn acc, old_acc -> {Enum.reverse(acc), old_acc} end + ] + + {[{"a", 1}, {"b", 2}], :ok, ""} = JSON.decode(~s({"a":1,"b":2}), :ok, decoders) + + 42 = JSON.decode!("42") + [1, nil, "test"] = JSON.decode!("[1,null,\"test\"]") + + :got_error = + try do + JSON.decode!("") + rescue + _e in [JSON.DecodeError] -> :got_error + end + + :got_error = + try do + JSON.decode!("x") + rescue + _e in [JSON.DecodeError] -> :got_error + end + + "null" = JSON.encode!(nil) + "true" = JSON.encode!(true) + "false" = JSON.encode!(false) + "42" = JSON.encode!(42) + "-1" = JSON.encode!(-1) + ~s("hello") = JSON.encode!("hello") + ~s("hello\\nworld") = JSON.encode!("hello\nworld") + + ~s("hello") = JSON.encode!(:hello) + + "[]" = JSON.encode!([]) + "[1,2,3]" = JSON.encode!([1, 2, 3]) + ~s([1,"two",true,null]) = JSON.encode!([1, "two", true, nil]) + + "{}" = JSON.encode!(%{}) + decoded = JSON.decode!(JSON.encode!(%{"a" => 1, "b" => 2})) + 1 = decoded["a"] + 2 = decoded["b"] + + atom_decoded = JSON.decode!(JSON.encode!(%{foo: 1, bar: 2})) + 1 = atom_decoded["foo"] + 2 = atom_decoded["bar"] + + nested = %{"a" => [1, %{"b" => true}]} + ^nested = JSON.decode!(JSON.encode!(nested)) + + iodata = JSON.encode_to_iodata!([1, 2, 3]) + "[1,2,3]" = IO.iodata_to_binary(iodata) + + terms = [42, -1, 3.14, true, false, nil, "hello", "", [], [1, 2, 3], %{}, %{"k" => "v"}] + + :ok = + Enum.each(terms, fn term -> + ^term = JSON.decode!(JSON.encode!(term)) + end) + + :ok + end end