From 50b00bac0f2f77db869bc5169385319ecb072fa9 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 14 Mar 2012 23:01:59 -0700 Subject: [PATCH 01/75] the option single_quotes in functions dealing with json inputs now allows json that uses single quotes to deliminate keys and strings to be processed, note that this changes the escaping rules slightly --- README.markdown | 11 +++++++++-- src/jsx.erl | 46 +++++++++++++++++++++++++++++++++++++++++++++ src/jsx_decoder.erl | 39 ++++++++++++++++++++++++++++++-------- src/jsx_opts.hrl | 2 +- src/jsx_utils.erl | 6 ++++-- 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/README.markdown b/README.markdown index 473882f..0464f00 100644 --- a/README.markdown +++ b/README.markdown @@ -32,6 +32,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `labels` - `{labels, Label}` - `Label` = @@ -40,7 +41,9 @@ types: * `existing_atom` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `labels` controls how keys are converted from json to erlang terms. `binary` does no conversion beyond normal escaping. `atom` converts keys to erlang atoms, and results in a badarg error if keys fall outside the range of erlang atoms. `existing_atom` is identical to `atom`, except it will not add new atoms to the atom table @@ -97,10 +100,13 @@ types: - `indent` - `{indent, N}` - `loose_unicode` + - `single_quotes` - `escape_forward_slash` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` @@ -125,6 +131,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `explicit_end` see `json_to_term` for details of options diff --git a/src/jsx.erl b/src/jsx.erl index 6c0dbe5..722f674 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -120,6 +120,52 @@ encoder_decoder_equiv_test_() -> ]. +single_quotes_test_() -> + [ + {"single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true}">>, [single_quotes]), + [{<<"key">>, true}] + ) + }, + {"multiple single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true, 'another key':true}">>, [single_quotes]), + [{<<"key">>, true}, {<<"another key">>, true}] + ) + }, + {"nested single quoted keys", + ?_assertEqual( + to_term(<<"{'key': {'key':true, 'another key':true}}">>, [single_quotes]), + [{<<"key">>, [{<<"key">>, true}, {<<"another key">>, true}]}] + ) + }, + {"single quoted string", + ?_assertEqual( + to_term(<<"['string']">>, [single_quotes]), + [<<"string">>] + ) + }, + {"single quote in double quoted string", + ?_assertEqual( + to_term(<<"[\"a single quote: '\"]">>), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote in single quoted string", + ?_assertEqual( + to_term(<<"['a single quote: \\'']">>, [single_quotes]), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote when single quotes are disallowed", + ?_assertError( + badarg, + to_term(<<"[\"a single quote: \\'\"]">>) + ) + } + ]. + %% test handler init([]) -> []. diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 0a82ea2..700ba0a 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -59,7 +59,8 @@ decoder(Handler, State, Opts) -> %% kv seperator -define(comma, 16#2C). --define(quote, 16#22). +-define(doublequote, 16#22). +-define(singlequote, 16#27). -define(colon, 16#3A). %% string escape sequences @@ -130,7 +131,9 @@ decoder(Handler, State, Opts) -> -define(end_seq(Seq), unicode:characters_to_binary(lists:reverse(Seq))). -value(<>, Handler, Stack, Opts) -> +value(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +value(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); value(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); @@ -156,7 +159,9 @@ value(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -object(<>, Handler, Stack, Opts) -> +object(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +object(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); object(<>, {Handler, State}, [key|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); @@ -168,7 +173,9 @@ object(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -array(<>, Handler, Stack, Opts) -> +array(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +array(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); array(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); @@ -206,7 +213,9 @@ colon(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -key(<>, Handler, Stack, Opts) -> +key(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +key(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); key(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> key(Rest, Handler, Stack, Opts); @@ -233,13 +242,25 @@ partial_utf(<>) partial_utf(_) -> false. -string(<>, {Handler, State}, [Acc, key|Stack], Opts) -> +string(<>, {Handler, State}, [Acc, key|Stack], Opts) -> colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts ); -string(<>, {Handler, State}, [Acc|Stack], Opts) -> +string(<>, {Handler, State}, [Acc, key|Stack], Opts = #opts{single_quotes=true}) -> + colon(Rest, + {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, + [key|Stack], + Opts + ); +string(<>, {Handler, State}, [Acc|Stack], Opts) -> + maybe_done(Rest, + {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, + Stack, + Opts + ); +string(<>, {Handler, State}, [Acc|Stack], Opts = #opts{single_quotes=true}) -> maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, @@ -318,8 +339,10 @@ escape(<<$t, Rest/binary>>, Handler, [Acc|Stack], Opts) -> escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); escape(<>, Handler, [Acc|Stack], Opts) - when S =:= ?quote; S =:= ?solidus; S =:= ?rsolidus -> + when S =:= ?doublequote; S =:= ?solidus; S =:= ?rsolidus -> string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?acc_seq(Acc, ?singlequote)|Stack], Opts); escape(<<>>, Handler, Stack, Opts) -> ?incomplete(escape, <<>>, Handler, Stack, Opts); escape(Bin, Handler, Stack, Opts) -> diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index d49254b..f60627f 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -2,5 +2,5 @@ loose_unicode = false, escape_forward_slash = false, explicit_end = false, - parser = auto + single_quotes = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 814092c..2c8b160 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -43,6 +43,8 @@ parse_opts([escape_forward_slash|Rest], Opts) -> parse_opts(Rest, Opts#opts{escape_forward_slash=true}); parse_opts([explicit_end|Rest], Opts) -> parse_opts(Rest, Opts#opts{explicit_end=true}); +parse_opts([single_quotes|Rest], Opts) -> + parse_opts(Rest, Opts#opts{single_quotes=true}); parse_opts(_, _) -> {error, badarg}. @@ -52,12 +54,12 @@ extract_opts(Opts) -> extract_parser_opts([], Acc) -> Acc; extract_parser_opts([{K,V}|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [{K,V}] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end; extract_parser_opts([K|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [K] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end. From c6827d06de2b69591576336fc9ad84a2f841d74d Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 14 Mar 2012 23:01:59 -0700 Subject: [PATCH 02/75] the option single_quotes in functions dealing with json inputs now allows json that uses single quotes to deliminate keys and strings to be processed, note that this changes the escaping rules slightly --- README.markdown | 11 ++++++++-- src/jsx.erl | 52 ++++++++++++++++++++++++++++++++++++++++++++ src/jsx_decoder.erl | 53 ++++++++++++++++++++++++++++++--------------- src/jsx_opts.hrl | 2 +- src/jsx_utils.erl | 6 +++-- 5 files changed, 101 insertions(+), 23 deletions(-) diff --git a/README.markdown b/README.markdown index 473882f..0464f00 100644 --- a/README.markdown +++ b/README.markdown @@ -32,6 +32,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `labels` - `{labels, Label}` - `Label` = @@ -40,7 +41,9 @@ types: * `existing_atom` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `labels` controls how keys are converted from json to erlang terms. `binary` does no conversion beyond normal escaping. `atom` converts keys to erlang atoms, and results in a badarg error if keys fall outside the range of erlang atoms. `existing_atom` is identical to `atom`, except it will not add new atoms to the atom table @@ -97,10 +100,13 @@ types: - `indent` - `{indent, N}` - `loose_unicode` + - `single_quotes` - `escape_forward_slash` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` @@ -125,6 +131,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `explicit_end` see `json_to_term` for details of options diff --git a/src/jsx.erl b/src/jsx.erl index 6c0dbe5..ff1bc09 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -120,6 +120,58 @@ encoder_decoder_equiv_test_() -> ]. +single_quotes_test_() -> + [ + {"single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true}">>, [single_quotes]), + [{<<"key">>, true}] + ) + }, + {"multiple single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true, 'another key':true}">>, [single_quotes]), + [{<<"key">>, true}, {<<"another key">>, true}] + ) + }, + {"nested single quoted keys", + ?_assertEqual( + to_term(<<"{'key': {'key':true, 'another key':true}}">>, [single_quotes]), + [{<<"key">>, [{<<"key">>, true}, {<<"another key">>, true}]}] + ) + }, + {"single quoted string", + ?_assertEqual( + to_term(<<"['string']">>, [single_quotes]), + [<<"string">>] + ) + }, + {"single quote in double quoted string", + ?_assertEqual( + to_term(<<"[\"a single quote: '\"]">>, [single_quotes]), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote in single quoted string", + ?_assertEqual( + to_term(<<"['a single quote: \\'']">>, [single_quotes]), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote when single quotes are disallowed", + ?_assertError( + badarg, + to_term(<<"[\"a single quote: \\'\"]">>) + ) + }, + {"mismatched quotes", + ?_assertError( + badarg, + to_term(<<"['mismatched\"]">>, [single_quotes]) + ) + } + ]. + %% test handler init([]) -> []. diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 0a82ea2..2cca939 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -59,7 +59,8 @@ decoder(Handler, State, Opts) -> %% kv seperator -define(comma, 16#2C). --define(quote, 16#22). +-define(doublequote, 16#22). +-define(singlequote, 16#27). -define(colon, 16#3A). %% string escape sequences @@ -130,8 +131,10 @@ decoder(Handler, State, Opts) -> -define(end_seq(Seq), unicode:characters_to_binary(lists:reverse(Seq))). -value(<>, Handler, Stack, Opts) -> +value(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +value(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); value(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); value(<<$f, Rest/binary>>, Handler, Stack, Opts) -> @@ -156,8 +159,10 @@ value(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -object(<>, Handler, Stack, Opts) -> +object(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +object(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); object(<>, {Handler, State}, [key|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); object(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> @@ -168,8 +173,10 @@ object(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -array(<>, Handler, Stack, Opts) -> +array(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +array(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); array(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); array(<<$f, Rest/binary>>, Handler, Stack, Opts) -> @@ -206,8 +213,10 @@ colon(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -key(<>, Handler, Stack, Opts) -> +key(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +key(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); key(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> key(Rest, Handler, Stack, Opts); key(<<>>, Handler, Stack, Opts) -> @@ -233,18 +242,24 @@ partial_utf(<>) partial_utf(_) -> false. -string(<>, {Handler, State}, [Acc, key|Stack], Opts) -> - colon(Rest, - {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, - [key|Stack], - Opts - ); -string(<>, {Handler, State}, [Acc|Stack], Opts) -> - maybe_done(Rest, - {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, - Stack, - Opts - ); +string(<>, {Handler, State}, S, Opts) -> + case S of + [Acc, key|Stack] -> + colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [Acc|Stack] -> + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts); + [Acc, single_quote|Stack] -> + ?error([<>, {Handler, State}, S, Opts]) + end; +string(<>, {Handler, State}, S, Opts = #opts{single_quotes=true}) -> + case S of + [Acc, single_quote, key|Stack] -> + colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [Acc, single_quote|Stack] -> + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts); + [Acc|Stack] -> + string(Rest, {Handler, State}, [?acc_seq(Acc, ?singlequote)|Stack], Opts) + end; string(<>, Handler, Stack, Opts) -> escape(Rest, Handler, Stack, Opts); %% things get dumb here. erlang doesn't properly restrict unicode non-characters @@ -318,8 +333,10 @@ escape(<<$t, Rest/binary>>, Handler, [Acc|Stack], Opts) -> escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); escape(<>, Handler, [Acc|Stack], Opts) - when S =:= ?quote; S =:= ?solidus; S =:= ?rsolidus -> + when S =:= ?doublequote; S =:= ?solidus; S =:= ?rsolidus -> string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?acc_seq(Acc, ?singlequote)|Stack], Opts); escape(<<>>, Handler, Stack, Opts) -> ?incomplete(escape, <<>>, Handler, Stack, Opts); escape(Bin, Handler, Stack, Opts) -> diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index d49254b..f60627f 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -2,5 +2,5 @@ loose_unicode = false, escape_forward_slash = false, explicit_end = false, - parser = auto + single_quotes = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 814092c..2c8b160 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -43,6 +43,8 @@ parse_opts([escape_forward_slash|Rest], Opts) -> parse_opts(Rest, Opts#opts{escape_forward_slash=true}); parse_opts([explicit_end|Rest], Opts) -> parse_opts(Rest, Opts#opts{explicit_end=true}); +parse_opts([single_quotes|Rest], Opts) -> + parse_opts(Rest, Opts#opts{single_quotes=true}); parse_opts(_, _) -> {error, badarg}. @@ -52,12 +54,12 @@ extract_opts(Opts) -> extract_parser_opts([], Acc) -> Acc; extract_parser_opts([{K,V}|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [{K,V}] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end; extract_parser_opts([K|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [K] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end. From 59689769de368e70bb730132b5de3fb3481510c0 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Thu, 15 Mar 2012 20:54:52 -0700 Subject: [PATCH 03/75] supress unused var errors --- src/jsx_decoder.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 2cca939..006cc22 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -246,10 +246,10 @@ string(<>, {Handler, State}, S, Opts) -> case S of [Acc, key|Stack] -> colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [_Acc, single_quote|_Stack] -> + ?error([<>, {Handler, State}, S, Opts]); [Acc|Stack] -> - maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts); - [Acc, single_quote|Stack] -> - ?error([<>, {Handler, State}, S, Opts]) + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts) end; string(<>, {Handler, State}, S, Opts = #opts{single_quotes=true}) -> case S of From a0657303802948577826539c627f5b63490e7b97 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 14 Mar 2012 23:01:59 -0700 Subject: [PATCH 04/75] the option single_quotes in functions dealing with json inputs now allows json that uses single quotes to deliminate keys and strings to be processed, note that this changes the escaping rules slightly --- README.markdown | 11 ++++++++-- src/jsx.erl | 52 ++++++++++++++++++++++++++++++++++++++++++++ src/jsx_decoder.erl | 53 ++++++++++++++++++++++++++++++--------------- src/jsx_opts.hrl | 2 +- src/jsx_utils.erl | 6 +++-- 5 files changed, 101 insertions(+), 23 deletions(-) diff --git a/README.markdown b/README.markdown index 1b3743f..28715b1 100644 --- a/README.markdown +++ b/README.markdown @@ -32,6 +32,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `labels` - `{labels, Label}` - `Label` = @@ -40,7 +41,9 @@ types: * `existing_atom` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `labels` controls how keys are converted from json to erlang terms. `binary` does no conversion beyond normal escaping. `atom` converts keys to erlang atoms, and results in a badarg error if keys fall outside the range of erlang atoms. `existing_atom` is identical to `atom`, except it will not add new atoms to the atom table @@ -97,10 +100,13 @@ types: - `indent` - `{indent, N}` - `loose_unicode` + - `single_quotes` - `escape_forward_slash` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` @@ -125,6 +131,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `explicit_end` see `json_to_term` for details of options diff --git a/src/jsx.erl b/src/jsx.erl index 6c0dbe5..ff1bc09 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -120,6 +120,58 @@ encoder_decoder_equiv_test_() -> ]. +single_quotes_test_() -> + [ + {"single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true}">>, [single_quotes]), + [{<<"key">>, true}] + ) + }, + {"multiple single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true, 'another key':true}">>, [single_quotes]), + [{<<"key">>, true}, {<<"another key">>, true}] + ) + }, + {"nested single quoted keys", + ?_assertEqual( + to_term(<<"{'key': {'key':true, 'another key':true}}">>, [single_quotes]), + [{<<"key">>, [{<<"key">>, true}, {<<"another key">>, true}]}] + ) + }, + {"single quoted string", + ?_assertEqual( + to_term(<<"['string']">>, [single_quotes]), + [<<"string">>] + ) + }, + {"single quote in double quoted string", + ?_assertEqual( + to_term(<<"[\"a single quote: '\"]">>, [single_quotes]), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote in single quoted string", + ?_assertEqual( + to_term(<<"['a single quote: \\'']">>, [single_quotes]), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote when single quotes are disallowed", + ?_assertError( + badarg, + to_term(<<"[\"a single quote: \\'\"]">>) + ) + }, + {"mismatched quotes", + ?_assertError( + badarg, + to_term(<<"['mismatched\"]">>, [single_quotes]) + ) + } + ]. + %% test handler init([]) -> []. diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 0a82ea2..2cca939 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -59,7 +59,8 @@ decoder(Handler, State, Opts) -> %% kv seperator -define(comma, 16#2C). --define(quote, 16#22). +-define(doublequote, 16#22). +-define(singlequote, 16#27). -define(colon, 16#3A). %% string escape sequences @@ -130,8 +131,10 @@ decoder(Handler, State, Opts) -> -define(end_seq(Seq), unicode:characters_to_binary(lists:reverse(Seq))). -value(<>, Handler, Stack, Opts) -> +value(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +value(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); value(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); value(<<$f, Rest/binary>>, Handler, Stack, Opts) -> @@ -156,8 +159,10 @@ value(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -object(<>, Handler, Stack, Opts) -> +object(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +object(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); object(<>, {Handler, State}, [key|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); object(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> @@ -168,8 +173,10 @@ object(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -array(<>, Handler, Stack, Opts) -> +array(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +array(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); array(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); array(<<$f, Rest/binary>>, Handler, Stack, Opts) -> @@ -206,8 +213,10 @@ colon(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -key(<>, Handler, Stack, Opts) -> +key(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); +key(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); key(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> key(Rest, Handler, Stack, Opts); key(<<>>, Handler, Stack, Opts) -> @@ -233,18 +242,24 @@ partial_utf(<>) partial_utf(_) -> false. -string(<>, {Handler, State}, [Acc, key|Stack], Opts) -> - colon(Rest, - {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, - [key|Stack], - Opts - ); -string(<>, {Handler, State}, [Acc|Stack], Opts) -> - maybe_done(Rest, - {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, - Stack, - Opts - ); +string(<>, {Handler, State}, S, Opts) -> + case S of + [Acc, key|Stack] -> + colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [Acc|Stack] -> + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts); + [Acc, single_quote|Stack] -> + ?error([<>, {Handler, State}, S, Opts]) + end; +string(<>, {Handler, State}, S, Opts = #opts{single_quotes=true}) -> + case S of + [Acc, single_quote, key|Stack] -> + colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [Acc, single_quote|Stack] -> + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts); + [Acc|Stack] -> + string(Rest, {Handler, State}, [?acc_seq(Acc, ?singlequote)|Stack], Opts) + end; string(<>, Handler, Stack, Opts) -> escape(Rest, Handler, Stack, Opts); %% things get dumb here. erlang doesn't properly restrict unicode non-characters @@ -318,8 +333,10 @@ escape(<<$t, Rest/binary>>, Handler, [Acc|Stack], Opts) -> escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); escape(<>, Handler, [Acc|Stack], Opts) - when S =:= ?quote; S =:= ?solidus; S =:= ?rsolidus -> + when S =:= ?doublequote; S =:= ?solidus; S =:= ?rsolidus -> string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?acc_seq(Acc, ?singlequote)|Stack], Opts); escape(<<>>, Handler, Stack, Opts) -> ?incomplete(escape, <<>>, Handler, Stack, Opts); escape(Bin, Handler, Stack, Opts) -> diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index d49254b..f60627f 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -2,5 +2,5 @@ loose_unicode = false, escape_forward_slash = false, explicit_end = false, - parser = auto + single_quotes = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 814092c..2c8b160 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -43,6 +43,8 @@ parse_opts([escape_forward_slash|Rest], Opts) -> parse_opts(Rest, Opts#opts{escape_forward_slash=true}); parse_opts([explicit_end|Rest], Opts) -> parse_opts(Rest, Opts#opts{explicit_end=true}); +parse_opts([single_quotes|Rest], Opts) -> + parse_opts(Rest, Opts#opts{single_quotes=true}); parse_opts(_, _) -> {error, badarg}. @@ -52,12 +54,12 @@ extract_opts(Opts) -> extract_parser_opts([], Acc) -> Acc; extract_parser_opts([{K,V}|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [{K,V}] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end; extract_parser_opts([K|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [K] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end. From dd917eb471127627bc8c4f6e0fb4ec0652448e2b Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Thu, 15 Mar 2012 20:54:52 -0700 Subject: [PATCH 05/75] supress unused var errors --- src/jsx_decoder.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 2cca939..006cc22 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -246,10 +246,10 @@ string(<>, {Handler, State}, S, Opts) -> case S of [Acc, key|Stack] -> colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [_Acc, single_quote|_Stack] -> + ?error([<>, {Handler, State}, S, Opts]); [Acc|Stack] -> - maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts); - [Acc, single_quote|Stack] -> - ?error([<>, {Handler, State}, S, Opts]) + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts) end; string(<>, {Handler, State}, S, Opts = #opts{single_quotes=true}) -> case S of From 30b5cea06d7e42e8877f561f4828e88ebf66d7f7 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Thu, 15 Mar 2012 22:56:21 -0700 Subject: [PATCH 06/75] escape strings and keys in the encoder --- src/jsx_encoder.erl | 29 ++++++++++++++++++++++------- src/jsx_utils.erl | 6 +++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 68bff84..f4d83e7 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -53,8 +53,8 @@ start(Term, {Handler, State}, Opts) -> Handler:handle_event(end_json, value(Term, {Handler, State}, Opts)). -value(String, {Handler, State}, _Opts) when is_binary(String) -> - Handler:handle_event({string, String}, State); +value(String, {Handler, State}, Opts) when is_binary(String) -> + Handler:handle_event({string, escape(String, {Handler, State}, Opts)}, State); value(Float, {Handler, State}, _Opts) when is_float(Float) -> Handler:handle_event({float, Float}, State); value(Int, {Handler, State}, _Opts) when is_integer(Int) -> @@ -78,9 +78,18 @@ list_or_object(List, {Handler, State}, Opts) -> object([{Key, Value}|Rest], {Handler, State}, Opts) -> - object(Rest, {Handler, - value(Value, {Handler, Handler:handle_event({key, fix_key(Key)}, State)}, Opts) - }, Opts); + object( + Rest, + { + Handler, + value( + Value, + {Handler, Handler:handle_event({key, escape(fix_key(Key), {Handler, State}, Opts)}, State)}, + Opts + ) + }, + Opts + ); object([], {Handler, State}, _Opts) -> Handler:handle_event(end_object, State); object(Term, Handler, Opts) -> ?error([Term, Handler, Opts]). @@ -91,8 +100,14 @@ list([], {Handler, State}, _Opts) -> Handler:handle_event(end_array, State); list(Term, Handler, Opts) -> ?error([Term, Handler, Opts]). -fix_key(Key) when is_binary(Key) -> Key; -fix_key(Key) when is_atom(Key) -> atom_to_binary(Key, utf8). +fix_key(Key) when is_atom(Key) -> fix_key(atom_to_binary(Key, utf8)); +fix_key(Key) when is_binary(Key) -> Key. + + +escape(String, Handler, Opts) -> + try jsx_utils:json_escape(String, Opts) + catch error:badarg -> erlang:error(badarg, [String, Handler, Opts]) + end. -ifdef(TEST). diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 2c8b160..fa6db97 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -97,7 +97,7 @@ json_escape(<<$\t, Rest/binary>>, Opts, Acc) -> json_escape(<>, Opts, Acc) when C >= 0, C < $\s -> json_escape(Rest, Opts, - <> + <> ); %% escape forward slashes -- optionally -- to faciliate microsoft's retarded %% date format @@ -108,7 +108,7 @@ json_escape(<>, Opts, Acc) when C == 16#2028; C == 16#2029 -> json_escape(Rest, Opts, - <> + <> ); %% any other legal codepoint json_escape(<>, Opts, Acc) -> @@ -122,7 +122,7 @@ json_escape(Rest, Opts, Acc) -> %% convert a codepoint to it's \uXXXX equiv. json_escape_sequence(X) -> <> = <>, - [$\\, $u, (to_hex(A)), (to_hex(B)), (to_hex(C)), (to_hex(D))]. + unicode:characters_to_binary([$\\, $u, (to_hex(A)), (to_hex(B)), (to_hex(C)), (to_hex(D))]). to_hex(10) -> $a; From 6392808d318ae3501869e43ce4d313ff5c5ae2d7 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Thu, 15 Mar 2012 23:06:19 -0700 Subject: [PATCH 07/75] add 'no_jsonp_escapes' flag/option to not escape u+2028 and u+2029 --- src/jsx_opts.hrl | 3 ++- src/jsx_utils.erl | 50 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index f60627f..938b8fb 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -2,5 +2,6 @@ loose_unicode = false, escape_forward_slash = false, explicit_end = false, - single_quotes = false + single_quotes = false, + no_jsonp_escapes = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index fa6db97..2ab87e9 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -45,21 +45,33 @@ parse_opts([explicit_end|Rest], Opts) -> parse_opts(Rest, Opts#opts{explicit_end=true}); parse_opts([single_quotes|Rest], Opts) -> parse_opts(Rest, Opts#opts{single_quotes=true}); +parse_opts([no_jsonp_escapes|Rest], Opts) -> + parse_opts(Rest, Opts#opts{no_jsonp_escapes=true}); parse_opts(_, _) -> {error, badarg}. +valid_flags() -> + [ + loose_unicode, + escape_forward_slash, + explicit_end, + single_quotes, + no_jsonp_escapes + ]. + + extract_opts(Opts) -> extract_parser_opts(Opts, []). extract_parser_opts([], Acc) -> Acc; extract_parser_opts([{K,V}|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of + case lists:member(K, valid_flags()) of true -> extract_parser_opts(Rest, [{K,V}] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end; extract_parser_opts([K|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of + case lists:member(K, valid_flags()) of true -> extract_parser_opts(Rest, [K] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end. @@ -103,6 +115,10 @@ json_escape(<>, Opts, Acc) when C >= 0, C < $\s -> %% date format json_escape(<<$/, Rest/binary>>, Opts=#opts{escape_forward_slash=true}, Acc) -> json_escape(Rest, Opts, <>); +%% skip escaping u+2028 and u+2029 +json_escape(<>, Opts=#opts{no_jsonp_escapes=true}, Acc) + when C == 16#2028; C == 16#2029 -> + json_escape(Rest, Opts, <>); %% escape u+2028 and u+2029 to avoid problems with jsonp json_escape(<>, Opts, Acc) when C == 16#2028; C == 16#2029 -> @@ -143,27 +159,33 @@ to_hex(X) -> X + 48. %% ascii "1" is [49], "2" is [50], etc... binary_escape_test_() -> [ {"json string escaping", - ?_assert(json_escape( - <<"\"\\\b\f\n\r\t">>, #opts{} - ) =:= <<"\\\"\\\\\\b\\f\\n\\r\\t">> + ?_assertEqual( + json_escape(<<"\"\\\b\f\n\r\t">>, #opts{}), + <<"\\\"\\\\\\b\\f\\n\\r\\t">> ) }, {"json string hex escape", - ?_assert(json_escape( - <<1, 2, 3, 11, 26, 30, 31>>, #opts{} - ) =:= <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> + ?_assertEqual( + json_escape(<<1, 2, 3, 11, 26, 30, 31>>, #opts{}), + <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> ) }, {"jsonp protection", - ?_assert(json_escape( - <<226, 128, 168, 226, 128, 169>>, #opts{} - ) =:= <<"\\u2028\\u2029">> + ?_assertEqual( + json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{}), + <<"\\u2028\\u2029">> + ) + }, + {"no jsonp escapes", + ?_assertEqual( + json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{no_jsonp_escapes=true}), + <<226, 128, 168, 226, 128, 169>> ) }, {"microsoft i hate your date format", - ?_assert(json_escape(<<"/Date(1303502009425)/">>, - #opts{escape_forward_slash=true} - ) =:= <<"\\/Date(1303502009425)\\/">> + ?_assertEqual( + json_escape(<<"/Date(1303502009425)/">>, #opts{escape_forward_slash=true}), + <<"\\/Date(1303502009425)\\/">> ) } ]. From 732dd407472b3c6457955dc8e174ccf6a6762575 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Fri, 16 Mar 2012 15:34:57 -0700 Subject: [PATCH 08/75] remove all ?_assert and replace with ?_assertFoo's --- src/jsx_encoder.erl | 73 +++++++------ src/jsx_to_json.erl | 258 ++++++++++++++++++++------------------------ src/jsx_to_term.erl | 54 +++++----- src/jsx_verify.erl | 6 +- 4 files changed, 176 insertions(+), 215 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index f4d83e7..0af674c 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -118,49 +118,48 @@ encode(Term) -> (encoder(jsx, [], []))(Term). encode_test_() -> [ - {"naked string", ?_assert(encode(<<"a string">>) - =:= [{string, <<"a string">>}, end_json]) - }, - {"naked integer", ?_assert(encode(123) - =:= [{integer, 123}, end_json]) - }, - {"naked float", ?_assert(encode(1.23) - =:= [{float, 1.23}, end_json]) - }, - {"naked literal", ?_assert(encode(null) - =:= [{literal, null}, end_json]) - }, - {"empty object", ?_assert(encode([{}]) - =:= [start_object, end_object, end_json]) - }, - {"empty list", ?_assert(encode([]) - =:= [start_array, end_array, end_json]) - }, - {"simple list", ?_assert(encode([1,2,3,true,false]) - =:= [start_array, + {"naked string", ?_assertEqual(encode(<<"a string">>), [{string, <<"a string">>}, end_json])}, + {"naked integer", ?_assertEqual(encode(123), [{integer, 123}, end_json])}, + {"naked float", ?_assertEqual(encode(1.23), [{float, 1.23}, end_json])}, + {"naked literal", ?_assertEqual(encode(null), [{literal, null}, end_json])}, + {"empty object", ?_assertEqual(encode([{}]), [start_object, end_object, end_json])}, + {"empty list", ?_assertEqual(encode([]), [start_array, end_array, end_json])}, + {"simple list", ?_assertEqual( + encode([1,2,3,true,false]), + [ + start_array, {integer, 1}, {integer, 2}, {integer, 3}, {literal, true}, {literal, false}, end_array, - end_json]) + end_json + ] + ) }, - {"simple object", ?_assert(encode([{<<"a">>, true}, {<<"b">>, false}]) - =:= [start_object, + {"simple object", ?_assertEqual( + encode([{<<"a">>, true}, {<<"b">>, false}]), + [ + start_object, {key, <<"a">>}, {literal, true}, {key, <<"b">>}, {literal, false}, end_object, - end_json]) + end_json + ] + ) }, - {"complex term", ?_assert(encode([ - {<<"a">>, true}, - {<<"b">>, false}, - {<<"c">>, [1,2,3]}, - {<<"d">>, [{<<"key">>, <<"value">>}]} - ]) =:= [start_object, + {"complex term", ?_assertEqual( + encode([ + {<<"a">>, true}, + {<<"b">>, false}, + {<<"c">>, [1,2,3]}, + {<<"d">>, [{<<"key">>, <<"value">>}]} + ]), + [ + start_object, {key, <<"a">>}, {literal, true}, {key, <<"b">>}, @@ -177,14 +176,14 @@ encode_test_() -> {string, <<"value">>}, end_object, end_object, - end_json]) + end_json + ] + ) }, - {"atom keys", ?_assert(encode([{key, <<"value">>}]) - =:= [start_object, - {key, <<"key">>}, - {string, <<"value">>}, - end_object, - end_json]) + {"atom keys", ?_assertEqual( + encode([{key, <<"value">>}]), + [start_object, {key, <<"key">>}, {string, <<"value">>}, end_object, end_json] + ) } ]. diff --git a/src/jsx_to_json.erl b/src/jsx_to_json.erl index 6d012f5..0e4edf4 100644 --- a/src/jsx_to_json.erl +++ b/src/jsx_to_json.erl @@ -186,176 +186,148 @@ teardown_nicedecimal_meck(_) -> basic_format_test_() -> [ - {"empty object", ?_assert(format(<<"{}">>, []) =:= <<"{}">>)}, - {"empty array", ?_assert(format(<<"[]">>, []) =:= <<"[]">>)}, - {"naked integer", ?_assert(format(<<"123">>, []) =:= <<"123">>)}, + {"empty object", ?_assertEqual(format(<<"{}">>, []), <<"{}">>)}, + {"empty array", ?_assertEqual(format(<<"[]">>, []), <<"[]">>)}, + {"naked integer", ?_assertEqual(format(<<"123">>, []), <<"123">>)}, {foreach, fun() -> setup_nicedecimal_meck(<<"1.23">>) end, fun(R) -> teardown_nicedecimal_meck(R) end, - [{"naked float", ?_assert(format(<<"1.23">>, []) =:= <<"1.23">>)}] - }, - {"naked string", ?_assert(format(<<"\"hi\"">>, []) =:= <<"\"hi\"">>)}, - {"naked literal", ?_assert(format(<<"true">>, []) =:= <<"true">>)}, - {"simple object", - ?_assert(format(<<" { \"key\" :\n\t \"value\"\r\r\r\n } ">>, - [] - ) =:= <<"{\"key\":\"value\"}">> - ) - }, - {"really simple object", - ?_assert(format(<<"{\"k\":\"v\"}">>, []) =:= <<"{\"k\":\"v\"}">>) - }, - {"nested object", - ?_assert(format(<<"{\"k\":{\"k\":\"v\"}, \"j\":{}}">>, [] - ) =:= <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> - ) - }, - {"simple array", - ?_assert(format(<<" [\n\ttrue,\n\tfalse , \n \tnull\n] ">>, - [] - ) =:= <<"[true,false,null]">> - ) - }, - {"really simple array", ?_assert(format(<<"[1]">>, []) =:= <<"[1]">>)}, - {"nested array", ?_assert(format(<<"[[[]]]">>, []) =:= <<"[[[]]]">>)}, - {"nested structures", - ?_assert(format( - <<"[{\"key\":\"value\", - \"another key\": \"another value\", - \"a list\": [true, false] - }, - [[{}]] - ]">>, [] - ) =:= <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> - ) + [{"naked float", ?_assertEqual(format(<<"1.23">>, []), <<"1.23">>)}] }, + {"naked string", ?_assertEqual(format(<<"\"hi\"">>, []), <<"\"hi\"">>)}, + {"naked literal", ?_assertEqual(format(<<"true">>, []), <<"true">>)}, + {"simple object", ?_assertEqual( + format(<<" { \"key\" :\n\t \"value\"\r\r\r\n } ">>, []), + <<"{\"key\":\"value\"}">> + )}, + {"really simple object", ?_assertEqual(format(<<"{\"k\":\"v\"}">>, []) , <<"{\"k\":\"v\"}">>)}, + {"nested object", ?_assertEqual( + format(<<"{\"k\":{\"k\":\"v\"}, \"j\":{}}">>, []), + <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> + )}, + {"simple array", ?_assertEqual( + format(<<" [\n\ttrue,\n\tfalse , \n \tnull\n] ">>, []), + <<"[true,false,null]">> + )}, + {"really simple array", ?_assertEqual(format(<<"[1]">>, []), <<"[1]">>)}, + {"nested array", ?_assertEqual(format(<<"[[[]]]">>, []), <<"[[[]]]">>)}, + {"nested structures", ?_assertEqual( + format(<<"[ + { + \"key\":\"value\", + \"another key\": \"another value\", + \"a list\": [true, false] + }, + [[{}]] + ]">>, []), + <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> + )}, {"simple nested structure", - ?_assert(format(<<"[[],{\"k\":[[],{}],\"j\":{}},[]]">>, [] - ) =:= <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> + ?_assertEqual( + format(<<"[[],{\"k\":[[],{}],\"j\":{}},[]]">>, []), + <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> ) } ]. basic_to_json_test_() -> [ - {"empty object", ?_assert(to_json([{}], []) =:= <<"{}">>)}, - {"empty array", ?_assert(to_json([], []) =:= <<"[]">>)}, - {"naked integer", ?_assert(to_json(123, []) =:= <<"123">>)}, + {"empty object", ?_assertEqual(to_json([{}], []), <<"{}">>)}, + {"empty array", ?_assertEqual(to_json([], []), <<"[]">>)}, + {"naked integer", ?_assertEqual(to_json(123, []), <<"123">>)}, {foreach, fun() -> setup_nicedecimal_meck(<<"1.23">>) end, fun(R) -> teardown_nicedecimal_meck(R) end, - [{"naked float", ?_assert(to_json(1.23, []) =:= <<"1.23">>)}] + [{"naked float", ?_assertEqual(to_json(1.23, []) , <<"1.23">>)}] }, - {"naked string", ?_assert(to_json(<<"hi">>, []) =:= <<"\"hi\"">>)}, - {"naked literal", ?_assert(to_json(true, []) =:= <<"true">>)}, - {"simple object", - ?_assert(to_json( - [{<<"key">>, <<"value">>}], - [] - ) =:= <<"{\"key\":\"value\"}">> - ) - }, - {"nested object", - ?_assert(to_json( - [{<<"k">>,[{<<"k">>,<<"v">>}]},{<<"j">>,[{}]}], - [] - ) =:= <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> - ) - }, - {"simple array", - ?_assert(to_json( - [true, false, null], - [] - ) =:= <<"[true,false,null]">> - ) - }, - {"really simple array", ?_assert(to_json([1], []) =:= <<"[1]">>)}, - {"nested array", ?_assert(to_json([[[]]], []) =:= <<"[[[]]]">>)}, - {"nested structures", - ?_assert(to_json( + {"naked string", ?_assertEqual(to_json(<<"hi">>, []), <<"\"hi\"">>)}, + {"naked literal", ?_assertEqual(to_json(true, []), <<"true">>)}, + {"simple object", ?_assertEqual( + to_json( + [{<<"key">>, <<"value">>}], + [] + ), + <<"{\"key\":\"value\"}">> + )}, + {"nested object", ?_assertEqual( + to_json( + [{<<"k">>,[{<<"k">>,<<"v">>}]},{<<"j">>,[{}]}], + [] + ), + <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> + )}, + {"simple array", ?_assertEqual(to_json([true, false, null], []), <<"[true,false,null]">>)}, + {"really simple array", ?_assertEqual(to_json([1], []), <<"[1]">>)}, + {"nested array", ?_assertEqual(to_json([[[]]], []), <<"[[[]]]">>)}, + {"nested structures", ?_assertEqual( + to_json( + [ [ - [ - {<<"key">>, <<"value">>}, - {<<"another key">>, <<"another value">>}, - {<<"a list">>, [true, false]} - ], - [[[{}]]] + {<<"key">>, <<"value">>}, + {<<"another key">>, <<"another value">>}, + {<<"a list">>, [true, false]} ], - [] - ) =:= <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> - ) - }, - {"simple nested structure", - ?_assert(to_json( - [[], [{<<"k">>, [[], [{}]]}, {<<"j">>, [{}]}], []], - [] - ) =:= <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> - ) - } + [[[{}]]] + ], + [] + ), + <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> + )}, + {"simple nested structure", ?_assertEqual( + to_json( + [[], [{<<"k">>, [[], [{}]]}, {<<"j">>, [{}]}], []], + [] + ), + <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> + )} ]. opts_test_() -> [ - {"unspecified indent/space", - ?_assert(format(<<" [\n\ttrue,\n\tfalse,\n\tnull\n] ">>, - [space, indent] - ) =:= <<"[\n true,\n false,\n null\n]">> - ) - }, - {"specific indent/space", - ?_assert(format( - <<"\n{\n\"key\" : [],\n\"another key\" : true\n}\n">>, - [{space, 2}, {indent, 3}] - ) =:= <<"{\n \"key\": [],\n \"another key\": true\n}">> - ) - }, - {"nested structures", - ?_assert(format( - <<"[{\"key\":\"value\", - \"another key\": \"another value\" - }, - [[true, false, null]] - ]">>, - [{space, 2}, {indent, 2}] - ) =:= <<"[\n {\n \"key\": \"value\",\n \"another key\": \"another value\"\n },\n [\n [\n true,\n false,\n null\n ]\n ]\n]">> - ) - }, - {"array spaces", - ?_assert(format(<<"[1,2,3]">>, - [{space, 2}] - ) =:= <<"[1, 2, 3]">> - ) - }, - {"object spaces", - ?_assert(format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, - [{space, 2}] - ) =:= <<"{\"a\": true, \"b\": true, \"c\": true}">> - ) - }, + {"unspecified indent/space", ?_assertEqual( + format(<<" [\n\ttrue,\n\tfalse,\n\tnull\n] ">>, [space, indent]), + <<"[\n true,\n false,\n null\n]">> + )}, + {"specific indent/space", ?_assertEqual( + format( + <<"\n{\n\"key\" : [],\n\"another key\" : true\n}\n">>, + [{space, 2}, {indent, 3}] + ), + <<"{\n \"key\": [],\n \"another key\": true\n}">> + )}, + {"nested structures", ?_assertEqual( + format( + <<"[{\"key\":\"value\", \"another key\": \"another value\"}, [[true, false, null]]]">>, + [{space, 2}, {indent, 2}] + ), + <<"[\n {\n \"key\": \"value\",\n \"another key\": \"another value\"\n },\n [\n [\n true,\n false,\n null\n ]\n ]\n]">> + )}, + {"array spaces", ?_assertEqual( + format(<<"[1,2,3]">>, [{space, 2}]), + <<"[1, 2, 3]">> + )}, + {"object spaces", ?_assertEqual( + format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, [{space, 2}]), + <<"{\"a\": true, \"b\": true, \"c\": true}">> + )}, {foreach, fun() -> setup_nicedecimal_meck(<<"1.23">>) end, fun(R) -> teardown_nicedecimal_meck(R) end, - [{ - "array indent", - ?_assert(format(<<"[1.23, 1.23, 1.23]">>, - [{indent, 2}] - ) =:= <<"[\n 1.23,\n 1.23,\n 1.23\n]">> - ) - }] + [{"array indent", ?_assertEqual( + format(<<"[1.23, 1.23, 1.23]">>, [{indent, 2}]), + <<"[\n 1.23,\n 1.23,\n 1.23\n]">> + )}] }, - {"object indent", - ?_assert(format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, - [{indent, 2}] - ) =:= <<"{\n \"a\":true,\n \"b\":true,\n \"c\":true\n}">> - ) - } + {"object indent", ?_assertEqual( + format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, [{indent, 2}]), + <<"{\n \"a\":true,\n \"b\":true,\n \"c\":true\n}">> + )} ]. ext_opts_test_() -> - [{"extopts", ?_assert(format(<<"[]">>, - [loose_unicode, {escape_forward_slash, true}] - ) =:= <<"[]">> - )} - ]. + [{"extopts", ?_assertEqual( + format(<<"[]">>, [loose_unicode, {escape_forward_slash, true}]), + <<"[]">> + )}]. -endif. \ No newline at end of file diff --git a/src/jsx_to_term.erl b/src/jsx_to_term.erl index 3099829..b5b73bc 100644 --- a/src/jsx_to_term.erl +++ b/src/jsx_to_term.erl @@ -102,35 +102,29 @@ format_key(Key, Opts) -> basic_test_() -> [ - {"empty object", ?_assert(to_term(<<"{}">>, []) =:= [{}])}, - {"simple object", ?_assert(to_term(<<"{\"key\": true}">>, []) =:= [{<<"key">>, true}])}, - {"less simple object", - ?_assert(to_term(<<"{\"a\": 1, \"b\": 2}">>, []) =:= [{<<"a">>, 1}, {<<"b">>, 2}]) - }, - {"nested object", - ?_assert(to_term(<<"{\"key\": {\"key\": true}}">>, []) =:= [{<<"key">>, [{<<"key">>, true}]}]) - }, + {"empty object", ?_assertEqual(to_term(<<"{}">>, []), [{}])}, + {"simple object", ?_assertEqual(to_term(<<"{\"key\": true}">>, []), [{<<"key">>, true}])}, + {"less simple object", ?_assertEqual( + to_term(<<"{\"a\": 1, \"b\": 2}">>, []), + [{<<"a">>, 1}, {<<"b">>, 2}] + )}, + {"nested object", ?_assertEqual( + to_term(<<"{\"key\": {\"key\": true}}">>, []), + [{<<"key">>, [{<<"key">>, true}]}] + )}, {"empty array", ?_assert(to_term(<<"[]">>, []) =:= [])}, - {"list of lists", - ?_assert(to_term(<<"[[],[],[]]">>, []) =:= [[], [], []]) - }, - {"list of strings", - ?_assert(to_term(<<"[\"hi\", \"there\"]">>, []) =:= [<<"hi">>, <<"there">>]) - }, - {"list of numbers", - ?_assert(to_term(<<"[1, 2.0, 3e4, -5]">>, []) =:= [1, 2.0, 3.0e4, -5]) - }, - {"list of literals", - ?_assert(to_term(<<"[true,false,null]">>, []) =:= [true,false,null]) - }, - {"list of objects", - ?_assert(to_term(<<"[{}, {\"a\":1, \"b\":2}, {\"key\":[true,false]}]">>, []) - =:= [[{}], [{<<"a">>,1},{<<"b">>,2}], [{<<"key">>,[true,false]}]]) - } + {"list of lists", ?_assertEqual(to_term(<<"[[],[],[]]">>, []), [[], [], []])}, + {"list of strings", ?_assertEqual(to_term(<<"[\"hi\", \"there\"]">>, []), [<<"hi">>, <<"there">>])}, + {"list of numbers", ?_assertEqual(to_term(<<"[1, 2.0, 3e4, -5]">>, []), [1, 2.0, 3.0e4, -5])}, + {"list of literals", ?_assertEqual(to_term(<<"[true,false,null]">>, []), [true,false,null])}, + {"list of objects", ?_assertEqual( + to_term(<<"[{}, {\"a\":1, \"b\":2}, {\"key\":[true,false]}]">>, []), + [[{}], [{<<"a">>,1},{<<"b">>,2}], [{<<"key">>,[true,false]}]] + )} ]. comprehensive_test_() -> - {"comprehensive test", ?_assert(to_term(comp_json(), []) =:= comp_term())}. + {"comprehensive test", ?_assertEqual(to_term(comp_json(), []), comp_term())}. comp_json() -> <<"[ @@ -157,7 +151,7 @@ comp_term() -> ]. atom_labels_test_() -> - {"atom labels test", ?_assert(to_term(comp_json(), [{labels, atom}]) =:= atom_term())}. + {"atom labels test", ?_assertEqual(to_term(comp_json(), [{labels, atom}]), atom_term())}. atom_term() -> [ @@ -173,10 +167,10 @@ atom_term() -> naked_test_() -> [ - {"naked integer", ?_assert(to_term(<<"123">>, []) =:= 123)}, - {"naked float", ?_assert(to_term(<<"-4.32e-17">>, []) =:= -4.32e-17)}, - {"naked literal", ?_assert(to_term(<<"true">>, []) =:= true)}, - {"naked string", ?_assert(to_term(<<"\"string\"">>, []) =:= <<"string">>)} + {"naked integer", ?_assertEqual(to_term(<<"123">>, []), 123)}, + {"naked float", ?_assertEqual(to_term(<<"-4.32e-17">>, []), -4.32e-17)}, + {"naked literal", ?_assertEqual(to_term(<<"true">>, []), true)}, + {"naked string", ?_assertEqual(to_term(<<"\"string\"">>, []), <<"string">>)} ]. -endif. \ No newline at end of file diff --git a/src/jsx_verify.erl b/src/jsx_verify.erl index d49c90f..1a91b37 100644 --- a/src/jsx_verify.erl +++ b/src/jsx_verify.erl @@ -169,11 +169,7 @@ term_true_test_() -> {"empty array", ?_assert(is_term([], []))}, {"whitespace", ?_assert(is_term([ true ], []))}, {"nested terms", - ?_assert(is_term( - [[{x, [[{}], [{}], [{}]]}, {y, [{}]}], [{}], [[[]]]], - [] - ) - ) + ?_assert(is_term([[{x, [[{}], [{}], [{}]]}, {y, [{}]}], [{}], [[[]]]], [])) }, {"numbers", ?_assert(is_term([-1.0, -1, -0, 0, 1.0e-1, 1, 1.0, 1.0e1], [])) From 8487bcc6509a4198e4711bd248d28fec19c0a19e Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 19 Mar 2012 14:34:07 -0700 Subject: [PATCH 09/75] allow c and c++ style comments anywhere whitespace is legal --- src/jsx_decoder.erl | 267 +++++++++++++++++++++++++++++++++++++++++++- src/jsx_opts.hrl | 3 +- src/jsx_utils.erl | 5 +- 3 files changed, 271 insertions(+), 4 deletions(-) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 006cc22..eb85efa 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -77,6 +77,9 @@ decoder(Handler, State, Opts) -> -define(negative, 16#2D). -define(positive, 16#2B). +%% comments +-define(star, 16#2A). + %% some useful guards -define(is_hex(Symbol), @@ -153,6 +156,9 @@ value(<>, {Handler, State}, Stack, Opts) -> array(Rest, {Handler, Handler:handle_event(start_array, State)}, [array|Stack], Opts); value(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> value(Rest, Handler, Stack, Opts); +value(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> value(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); value(<<>>, Handler, Stack, Opts) -> ?incomplete(value, <<>>, Handler, Stack, Opts); value(Bin, Handler, Stack, Opts) -> @@ -167,6 +173,9 @@ object(<>, {Handler, State}, [key|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); object(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> object(Rest, Handler, Stack, Opts); +object(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> object(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); object(<<>>, Handler, Stack, Opts) -> ?incomplete(object, <<>>, Handler, Stack, Opts); object(Bin, Handler, Stack, Opts) -> @@ -196,7 +205,10 @@ array(<>, {Handler, State}, Stack, Opts) -> array(<>, {Handler, State}, [array|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_array, State)}, Stack, Opts); array(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> - array(Rest, Handler, Stack, Opts); + array(Rest, Handler, Stack, Opts); +array(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> array(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); array(<<>>, Handler, Stack, Opts) -> ?incomplete(array, <<>>, Handler, Stack, Opts); array(Bin, Handler, Stack, Opts) -> @@ -207,6 +219,9 @@ colon(<>, Handler, [key|Stack], Opts) -> value(Rest, Handler, [object|Stack], Opts); colon(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> colon(Rest, Handler, Stack, Opts); +colon(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> colon(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); colon(<<>>, Handler, Stack, Opts) -> ?incomplete(colon, <<>>, Handler, Stack, Opts); colon(Bin, Handler, Stack, Opts) -> @@ -218,7 +233,10 @@ key(<>, Handler, Stack, Opts) -> key(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); key(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> - key(Rest, Handler, Stack, Opts); + key(Rest, Handler, Stack, Opts); +key(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> key(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); key(<<>>, Handler, Stack, Opts) -> ?incomplete(key, <<>>, Handler, Stack, Opts); key(Bin, Handler, Stack, Opts) -> @@ -478,6 +496,9 @@ zero(<>, Handler, [Acc|Stack], Opts) -> initial_decimal(Rest, Handler, [{Acc, []}|Stack], Opts); zero(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +zero(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); zero(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); zero(<<>>, Handler, Stack, Opts) -> @@ -516,6 +537,9 @@ integer(<>, Handler, [Acc|Stack], Opts) when S =:= $e; S =:= $E e(Rest, Handler, [{Acc, [], []}|Stack], Opts); integer(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +integer(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); integer(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); integer(<<>>, Handler, Stack, Opts) -> @@ -561,6 +585,9 @@ decimal(<>, Handler, [{Int, Frac}|Stack], Opts) e(Rest, Handler, [{Int, Frac, []}|Stack], Opts); decimal(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +decimal(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); decimal(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); decimal(<<>>, Handler, Stack, Opts) -> @@ -615,6 +642,9 @@ exp(<>, {Handler, State}, [Acc, array|Stack], Opts) -> value(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [array|Stack], Opts); exp(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +exp(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); exp(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); exp(<<>>, Handler, Stack, Opts) -> @@ -713,6 +743,48 @@ null(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). +comment(<>, Handler, Stack, Opts) -> + single_comment(Rest, Handler, Stack, Opts); +comment(<>, Handler, Stack, Opts) -> + multi_comment(Rest, Handler, Stack, Opts); +comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(comment, <<>>, Handler, Stack, Opts); +comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + +single_comment(<>, Handler, [Resume|Stack], Opts) -> + Resume(Rest, Handler, Stack, Opts); +single_comment(<<>>, Handler, [Resume|Stack], Opts) -> + Resume(<<>>, Handler, Stack, Opts); +single_comment(<<_S/utf8, Rest/binary>>, Handler, Stack, Opts) -> + single_comment(Rest, Handler, Stack, Opts); +single_comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(single_comment, <<>>, Handler, Stack, Opts); +single_comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + +multi_comment(<>, Handler, Stack, Opts) -> + end_multi_comment(Rest, Handler, Stack, Opts); +multi_comment(<<_S/utf8, Rest/binary>>, Handler, Stack, Opts) -> + multi_comment(Rest, Handler, Stack, Opts); +multi_comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(multi_comment, <<>>, Handler, Stack, Opts); +multi_comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + +end_multi_comment(<>, Handler, [Resume|Stack], Opts) -> + Resume(Rest, Handler, Stack, Opts); +end_multi_comment(<<_S/utf8, Rest/binary>>, Handler, Stack, Opts) -> + multi_comment(Rest, Handler, Stack, Opts); +end_multi_comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(end_multi_comment, <<>>, Handler, Stack, Opts); +end_multi_comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + maybe_done(<>, {Handler, State}, [object|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); maybe_done(<>, {Handler, State}, [array|Stack], Opts) -> @@ -723,6 +795,9 @@ maybe_done(<>, Handler, [array|_] = Stack, Opts) -> value(Rest, Handler, Stack, Opts); maybe_done(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> maybe_done(Rest, Handler, Stack, Opts); +maybe_done(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); maybe_done(<<>>, Handler, Stack, Opts) when length(Stack) > 0 -> ?incomplete(maybe_done, <<>>, Handler, Stack, Opts); maybe_done(Rest, {Handler, State}, [], Opts) -> @@ -733,6 +808,9 @@ maybe_done(Bin, Handler, Stack, Opts) -> done(<>, Handler, [], Opts) when ?is_whitespace(S) -> done(Rest, Handler, [], Opts); +done(<>, Handler, [], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> done(R, H, S, O) end, + comment(Rest, Handler, [Resume], Opts); done(<<>>, {Handler, State}, [], Opts = #opts{explicit_end=true}) -> {incomplete, fun(Stream) when is_binary(Stream) -> done(<>, {Handler, State}, [], Opts) @@ -748,6 +826,191 @@ done(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -include_lib("eunit/include/eunit.hrl"). +comments_test_() -> + [ + {"preceeding // comment", ?_assertEqual( + decode(<<"// comment ", ?newline, "[]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"preceeding /**/ comment", ?_assertEqual( + decode(<<"/* comment */[]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"trailing // comment", ?_assertEqual( + decode(<<"[]// comment", ?newline>>, [comments]), + [start_array, end_array, end_json] + )}, + {"trailing // comment (no newline)", ?_assertEqual( + decode(<<"[]// comment">>, [comments]), + [start_array, end_array, end_json] + )}, + {"trailing /**/ comment", ?_assertEqual( + decode(<<"[] /* comment */">>, [comments]), + [start_array, end_array, end_json] + )}, + {"// comment inside array", ?_assertEqual( + decode(<<"[ // comment", ?newline, "]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"/**/ comment inside array", ?_assertEqual( + decode(<<"[ /* comment */ ]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"// comment at beginning of array", ?_assertEqual( + decode(<<"[ // comment", ?newline, "true", ?newline, "]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"/**/ comment at beginning of array", ?_assertEqual( + decode(<<"[ /* comment */ true ]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"// comment at end of array", ?_assertEqual( + decode(<<"[ true // comment", ?newline, "]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"/**/ comment at end of array", ?_assertEqual( + decode(<<"[ true /* comment */ ]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"// comment midarray (post comma)", ?_assertEqual( + decode(<<"[ true, // comment", ?newline, "false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"/**/ comment midarray (post comma)", ?_assertEqual( + decode(<<"[ true, /* comment */ false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"// comment midarray (pre comma)", ?_assertEqual( + decode(<<"[ true// comment", ?newline, ", false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"/**/ comment midarray (pre comma)", ?_assertEqual( + decode(<<"[ true/* comment */, false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"// comment inside object", ?_assertEqual( + decode(<<"{ // comment", ?newline, "}">>, [comments]), + [start_object, end_object, end_json] + )}, + {"/**/ comment inside object", ?_assertEqual( + decode(<<"{ /* comment */ }">>, [comments]), + [start_object, end_object, end_json] + )}, + {"// comment at beginning of object", ?_assertEqual( + decode(<<"{ // comment", ?newline, " \"key\": true", ?newline, "}">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment at beginning of object", ?_assertEqual( + decode(<<"{ /* comment */ \"key\": true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment at end of object", ?_assertEqual( + decode(<<"{ \"key\": true // comment", ?newline, "}">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment at end of object", ?_assertEqual( + decode(<<"{ \"key\": true /* comment */ }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment midobject (post comma)", ?_assertEqual( + decode(<<"{ \"x\": true, // comment", ?newline, "\"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"/**/ comment midobject (post comma)", ?_assertEqual( + decode(<<"{ \"x\": true, /* comment */", ?newline, "\"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"// comment midobject (pre comma)", ?_assertEqual( + decode(<<"{ \"x\": true// comment", ?newline, ", \"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"/**/ comment midobject (pre comma)", ?_assertEqual( + decode(<<"{ \"x\": true/* comment */", ?newline, ", \"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"// comment precolon", ?_assertEqual( + decode(<<"{ \"key\" // comment", ?newline, ": true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment precolon", ?_assertEqual( + decode(<<"{ \"key\"/* comment */: true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment postcolon", ?_assertEqual( + decode(<<"{ \"key\": // comment", ?newline, " true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment postcolon", ?_assertEqual( + decode(<<"{ \"key\":/* comment */ true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment terminating zero", ?_assertEqual( + decode(<<"[ 0// comment", ?newline, "]">>, [comments]), + [start_array, {integer, 0}, end_array, end_json] + )}, + {"// comment terminating integer", ?_assertEqual( + decode(<<"[ 1// comment", ?newline, "]">>, [comments]), + [start_array, {integer, 1}, end_array, end_json] + )}, + {"// comment terminating float", ?_assertEqual( + decode(<<"[ 1.0// comment", ?newline, "]">>, [comments]), + [start_array, {float, 1.0}, end_array, end_json] + )}, + {"// comment terminating exp", ?_assertEqual( + decode(<<"[ 1e1// comment", ?newline, "]">>, [comments]), + [start_array, {float, 1.0e1}, end_array, end_json] + )}, + {"/**/ comment terminating zero", ?_assertEqual( + decode(<<"[ 0/* comment */ ]">>, [comments]), + [start_array, {integer, 0}, end_array, end_json] + )}, + {"/**/ comment terminating integer", ?_assertEqual( + decode(<<"[ 1/* comment */ ]">>, [comments]), + [start_array, {integer, 1}, end_array, end_json] + )}, + {"/**/ comment terminating float", ?_assertEqual( + decode(<<"[ 1.0/* comment */ ]">>, [comments]), + [start_array, {float, 1.0}, end_array, end_json] + )}, + {"/**/ comment terminating exp", ?_assertEqual( + decode(<<"[ 1e1/* comment */ ]">>, [comments]), + [start_array, {float, 1.0e1}, end_array, end_json] + )} + ]. + + noncharacters_test_() -> [ {"noncharacters - badjson", diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index 938b8fb..7741585 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -3,5 +3,6 @@ escape_forward_slash = false, explicit_end = false, single_quotes = false, - no_jsonp_escapes = false + no_jsonp_escapes = false, + comments = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 2ab87e9..21f74d2 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -47,6 +47,8 @@ parse_opts([single_quotes|Rest], Opts) -> parse_opts(Rest, Opts#opts{single_quotes=true}); parse_opts([no_jsonp_escapes|Rest], Opts) -> parse_opts(Rest, Opts#opts{no_jsonp_escapes=true}); +parse_opts([comments|Rest], Opts) -> + parse_opts(Rest, Opts#opts{comments=true}); parse_opts(_, _) -> {error, badarg}. @@ -57,7 +59,8 @@ valid_flags() -> escape_forward_slash, explicit_end, single_quotes, - no_jsonp_escapes + no_jsonp_escapes, + comments ]. From e852286e9b21d8bc19380cf3f778d3338ee96f99 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 19 Mar 2012 15:57:00 -0700 Subject: [PATCH 10/75] apply escape_forward_slash option to decoding as well as encoding --- priv/test_cases/string_escapes.json | 2 +- priv/test_cases/string_escapes.test | 1 - src/jsx_decoder.erl | 22 +++++++++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/priv/test_cases/string_escapes.json b/priv/test_cases/string_escapes.json index 461bc67..3c9af78 100644 --- a/priv/test_cases/string_escapes.json +++ b/priv/test_cases/string_escapes.json @@ -1 +1 @@ -["\"", "\\", "\/", "\b", "\f", "\n", "\r", "\t"] \ No newline at end of file +["\"", "\\", "\b", "\f", "\n", "\r", "\t"] \ No newline at end of file diff --git a/priv/test_cases/string_escapes.test b/priv/test_cases/string_escapes.test index 7cd460c..8f6eeed 100644 --- a/priv/test_cases/string_escapes.test +++ b/priv/test_cases/string_escapes.test @@ -2,7 +2,6 @@ {jsx, [start_array, {string,<<"\"">>}, {string,<<"\\">>}, - {string,<<"/">>}, {string,<<"\b">>}, {string,<<"\f">>}, {string,<<"\n">>}, diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index eb85efa..4055b2e 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -348,13 +348,16 @@ escape(<<$r, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, $\r)|Stack], Opts); escape(<<$t, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, $\t)|Stack], Opts); -escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> - escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); -escape(<>, Handler, [Acc|Stack], Opts) - when S =:= ?doublequote; S =:= ?solidus; S =:= ?rsolidus -> - string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, $\\)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts=#opts{escape_forward_slash=true}) -> + string(Rest, Handler, [?acc_seq(Acc, $/)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, $\")|Stack], Opts); escape(<>, Handler, [Acc|Stack], Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?acc_seq(Acc, ?singlequote)|Stack], Opts); +escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> + escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); escape(<<>>, Handler, Stack, Opts) -> ?incomplete(escape, <<>>, Handler, Stack, Opts); escape(Bin, Handler, Stack, Opts) -> @@ -1011,6 +1014,15 @@ comments_test_() -> ]. +escape_forward_slash_test_() -> + [ + {"escape forward slash test", ?_assertEqual( + decode(<<"[ \" \/ \" ]">>, [escape_forward_slash]), + [start_array, {string, <<" / ">>}, end_array, end_json] + )} + ]. + + noncharacters_test_() -> [ {"noncharacters - badjson", From 5e2076065628d6440de0892265362fa42412bdbe Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 19 Mar 2012 16:01:58 -0700 Subject: [PATCH 11/75] apply loose_unicode option to decoder --- src/jsx_utils.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 21f74d2..2c4526e 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -132,6 +132,8 @@ json_escape(<>, Opts, Acc) %% any other legal codepoint json_escape(<>, Opts, Acc) -> json_escape(Rest, Opts, <>); +json_escape(<<_, Rest/binary>>, Opts=#opts{loose_unicode=true}, Acc) -> + json_escape(Rest, Opts, <>); json_escape(<<>>, _Opts, Acc) -> Acc; json_escape(Rest, Opts, Acc) -> @@ -173,6 +175,12 @@ binary_escape_test_() -> <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> ) }, + {"json string loose unicode escaping", + ?_assertEqual( + json_escape(<<16#ffff>>, #opts{loose_unicode=true}), + <<16#fffd/utf8>> + ) + }, {"jsonp protection", ?_assertEqual( json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{}), From a8254887f5024f6a1a866f2f74e55d21923258b1 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 19:36:27 -0700 Subject: [PATCH 12/75] fixes wrongheaded and stupid escaping of strings --- src/jsx_encoder.erl | 85 +++++++++++++++++++++++++++++++++++++++++---- src/jsx_utils.erl | 22 +++--------- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 0af674c..f97bf02 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -25,7 +25,6 @@ -export([encoder/3]). - -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). encoder(Handler, State, Opts) -> @@ -54,7 +53,7 @@ start(Term, {Handler, State}, Opts) -> value(String, {Handler, State}, Opts) when is_binary(String) -> - Handler:handle_event({string, escape(String, {Handler, State}, Opts)}, State); + Handler:handle_event({string, check_string(String, {Handler, State}, Opts)}, State); value(Float, {Handler, State}, _Opts) when is_float(Float) -> Handler:handle_event({float, Float}, State); value(Int, {Handler, State}, _Opts) when is_integer(Int) -> @@ -84,7 +83,7 @@ object([{Key, Value}|Rest], {Handler, State}, Opts) -> Handler, value( Value, - {Handler, Handler:handle_event({key, escape(fix_key(Key), {Handler, State}, Opts)}, State)}, + {Handler, Handler:handle_event({key, check_string(fix_key(Key), {Handler, State}, Opts)}, State)}, Opts ) }, @@ -104,10 +103,70 @@ fix_key(Key) when is_atom(Key) -> fix_key(atom_to_binary(Key, utf8)); fix_key(Key) when is_binary(Key) -> Key. -escape(String, Handler, Opts) -> - try jsx_utils:json_escape(String, Opts) - catch error:badarg -> erlang:error(badarg, [String, Handler, Opts]) - end. +check_string(String, Handler, Opts) -> + case check_string(String) of + true -> String; + false -> + case Opts#opts.loose_unicode of + true -> clean_string(String, <<>>); + false -> erlang:error(badarg, [String, Handler, Opts]) + end + end. + +check_string(<>) when C < 16#fdd0 -> + check_string(Rest); +check_string(<>) when C > 16#fdef, C < 16#fffe -> + check_string(Rest); +check_string(<>) + when C =/= 16#fffe andalso C =/= 16#ffff andalso + C =/= 16#1fffe andalso C =/= 16#1ffff andalso + C =/= 16#2fffe andalso C =/= 16#2ffff andalso + C =/= 16#3fffe andalso C =/= 16#3ffff andalso + C =/= 16#4fffe andalso C =/= 16#4ffff andalso + C =/= 16#5fffe andalso C =/= 16#5ffff andalso + C =/= 16#6fffe andalso C =/= 16#6ffff andalso + C =/= 16#7fffe andalso C =/= 16#7ffff andalso + C =/= 16#8fffe andalso C =/= 16#8ffff andalso + C =/= 16#9fffe andalso C =/= 16#9ffff andalso + C =/= 16#afffe andalso C =/= 16#affff andalso + C =/= 16#bfffe andalso C =/= 16#bffff andalso + C =/= 16#cfffe andalso C =/= 16#cffff andalso + C =/= 16#dfffe andalso C =/= 16#dffff andalso + C =/= 16#efffe andalso C =/= 16#effff andalso + C =/= 16#ffffe andalso C =/= 16#fffff andalso + C =/= 16#10fffe andalso C =/= 16#10ffff -> + check_string(Rest); +check_string(<<>>) -> true; +check_string(<<_, _/binary>>) -> false. + +clean_string(<>, Acc) when C >= 16#fdd0, C =< 16#fdef -> + io:format("1: ~p~n", [C]), + clean_string(Rest, <>); +clean_string(<>, Acc) + when C == 16#fffe orelse C == 16#ffff orelse + C == 16#1fffe orelse C == 16#1ffff orelse + C == 16#2fffe orelse C == 16#2ffff orelse + C == 16#3fffe orelse C == 16#3ffff orelse + C == 16#4fffe orelse C == 16#4ffff orelse + C == 16#5fffe orelse C == 16#5ffff orelse + C == 16#6fffe orelse C == 16#6ffff orelse + C == 16#7fffe orelse C == 16#7ffff orelse + C == 16#8fffe orelse C == 16#8ffff orelse + C == 16#9fffe orelse C == 16#9ffff orelse + C == 16#afffe orelse C == 16#affff orelse + C == 16#bfffe orelse C == 16#bffff orelse + C == 16#cfffe orelse C == 16#cffff orelse + C == 16#dfffe orelse C == 16#dffff orelse + C == 16#efffe orelse C == 16#effff orelse + C == 16#ffffe orelse C == 16#fffff orelse + C == 16#10fffe orelse C == 16#10ffff -> + io:format("2: ~p~n", [C]), + clean_string(Rest, <>); +clean_string(<>, Acc) -> + io:format("3: ~p~n", [C]), + clean_string(Rest, <>); +clean_string(<<>>, Acc) -> Acc. + -ifdef(TEST). @@ -115,6 +174,8 @@ escape(String, Handler, Opts) -> encode(Term) -> (encoder(jsx, [], []))(Term). +encode(Term, Opts) -> (encoder(jsx, [], Opts))(Term). + encode_test_() -> [ @@ -184,6 +245,16 @@ encode_test_() -> encode([{key, <<"value">>}]), [start_object, {key, <<"key">>}, {string, <<"value">>}, end_object, end_json] ) + }, + {"bad string", ?_assertError( + badarg, + encode([<<"a bad string: ", 16#ffff/utf8>>]) + ) + }, + {"allow bad string", ?_assertEqual( + encode([<<"a bad string: ", 16#1ffff/utf8>>], [loose_unicode]), + [start_array, {string, <<"a bad string: ", 16#fffd/utf8>>}, end_array, end_json] + ) } ]. diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 2c4526e..0ca6e8c 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -110,10 +110,7 @@ json_escape(<<$\t, Rest/binary>>, Opts, Acc) -> json_escape(Rest, Opts, <>); %% other control characters json_escape(<>, Opts, Acc) when C >= 0, C < $\s -> - json_escape(Rest, - Opts, - <> - ); + json_escape(Rest, Opts, <>); %% escape forward slashes -- optionally -- to faciliate microsoft's retarded %% date format json_escape(<<$/, Rest/binary>>, Opts=#opts{escape_forward_slash=true}, Acc) -> @@ -125,19 +122,14 @@ json_escape(<>, Opts=#opts{no_jsonp_escapes=true}, Acc) %% escape u+2028 and u+2029 to avoid problems with jsonp json_escape(<>, Opts, Acc) when C == 16#2028; C == 16#2029 -> - json_escape(Rest, - Opts, - <> - ); + json_escape(Rest, Opts, <>); %% any other legal codepoint json_escape(<>, Opts, Acc) -> json_escape(Rest, Opts, <>); -json_escape(<<_, Rest/binary>>, Opts=#opts{loose_unicode=true}, Acc) -> - json_escape(Rest, Opts, <>); json_escape(<<>>, _Opts, Acc) -> Acc; -json_escape(Rest, Opts, Acc) -> - erlang:error(badarg, [Rest, Opts, Acc]). +json_escape(Bin, Opts, Acc) -> + erlang:error(badarg, [Bin, Opts, Acc]). %% convert a codepoint to it's \uXXXX equiv. @@ -175,12 +167,6 @@ binary_escape_test_() -> <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> ) }, - {"json string loose unicode escaping", - ?_assertEqual( - json_escape(<<16#ffff>>, #opts{loose_unicode=true}), - <<16#fffd/utf8>> - ) - }, {"jsonp protection", ?_assertEqual( json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{}), From 6f63b1183fbb8dbd253f2c6fa2af758e65ac4b94 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 19:37:02 -0700 Subject: [PATCH 13/75] largely rewritten readme, hopefully more useful --- README.markdown | 314 +++++++++++++++++++++++++++++++----------------- 1 file changed, 201 insertions(+), 113 deletions(-) diff --git a/README.markdown b/README.markdown index 28715b1..b8da7de 100644 --- a/README.markdown +++ b/README.markdown @@ -1,209 +1,297 @@ -## jsx (v1.0) ## +# jsx (v1.0) # -a sane json implementation for erlang, inspired by [yajl][yajl] +a sane [json][json] implementation for erlang, inspired by [yajl][yajl] copyright 2011, 2012 alisdair sullivan jsx is released under the terms of the [MIT][MIT] license -jsx uses [rebar][rebar] and [meck][meck] +jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suite -[![Build Status](https://secure.travis-ci.org/talentdeficit/jsx.png?branch=master)](http://travis-ci.org/talentdeficit/jsx) +[![Build Status](https://secure.travis-ci.org/talentdeficit/jsx.png?branch=develop)](http://travis-ci.org/talentdeficit/jsx) -## api ## +## index ## + +* #### [introduction](#intro) #### +* #### [quickstart](#quickstart) #### +* #### [the api](#api) #### + - [json <-> erlang mapping](#mapping) + - [options](#options) + - [incomplete input](#incompletes) + - [the encoder and decoder](#core) + - [handler callbacks](#handler) + - [converting json to erlang and vice versa](#convert) + - [formatting and minifying json text](#format) + - [verifying json and terms are valid input](#verify) + * #### [acknowledgments](#thanks) #### -**converting json to erlang terms** -parses a JSON text (a utf8 encoded binary) and produces an erlang term (see json <-> erlang mapping details below) +## quickstart ## + +to build the library: `rebar compile` + +to convert a utf8 binary containing a json string into an erlang term: `jsx:to_term(JSON)` + +to convert an erlang term into a utf8 binary containing a json string: `jsx:to_json(Term)` + +to check if a binary is valid json: `jsx:is_json(JSON)` + +to check if a term is valid json: `jsx:is_term(Term)` + +to minify a json string: `jsx:format(JSON)` + + +## api ## + + +### json <-> erlang mapping ### + +**json** | **erlang** +--------------------------------|-------------------------------- +`number` | `integer()` and `float()` +`string` | `binary()` +`true`, `false` and `null` | `true`, `false` and `null` +`array` | `[]` and `[JSON]` +`object` | `[{}]` and `[{binary() OR atom(), JSON}]` + +#### json #### + +json must be a binary encoded in `utf8`. if it's invalid `utf8` or invalid json, it probably won't parse without errors. there are a few non-standard extensions to the parser available that may change that, they are detailed in the options section below + +jsx also supports json fragments; valid json values that are not complete json. that means jsx will parse things like `<<"1">`, `<<"true">>` and `<<"\"hello world\"">>` without problems + +#### erlang #### + +only the erlang terms in the table above are supported. non supported terms result in badarg errors. jsx is never going to support erlang lists instead of binaries, mostly because you can't discriminate between lists of integers and strings without hinting, and hinting is silly + +#### numbers #### + +javascript and thus json represent all numeric values with floats. as this is woefully insufficient for many uses, **jsx**, just like erlang, supports bigints. whenever possible, this library will interpret json numbers that look like integers as integers. other numbers will be converted to erlang's floating point type, which is nearly but not quite iee754. negative zero is not representable in erlang (zero is unsigned in erlang and `0` is equivalent to `-0`) and will be interpreted as regular zero. numbers not representable are beyond the concern of this implementation, and will result in parsing errors + +when converting from erlang to json, numbers are represented with their shortest representation that will round trip without loss of precision. this means that some floats may be superficially dissimilar (although functionally equivalent). for example, `1.0000000000000001` will be represented by `1.0` + +#### strings #### + +the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters and quotes) and that are encoded in `utf8`. in the interests of pragmatism, there is an option for looser parsing, see options below + +all erlang strings are represented by *valid* `utf8` encoded binaries + +this implementation performs no normalization on strings beyond that detailed here. be careful when comparing strings as equivalent strings may have different `utf8` encodings + +#### true, false and null #### + +the json primitives `true`, `false` and `null` are represented by the erlang atoms `true`, `false` and `null`. surprise + +#### arrays #### + +json arrays are represented with erlang lists of json values as described in this section + +#### objects #### + +json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties. all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers) + + +### options ### + +jsx functions all take a common set of options. not all flags have meaning in all contexts, but they are always valid options. flags are always atoms and have no value. functions may have additional options beyond these, see individual function documentation for details + +#### `loose_unicode` #### + +json text input and json strings SHOULD be utf8 encoded binaries, appropriately escaped as per the json spec. if this option is present attempts are made to replace invalid codepoints with `u+FFFD` as per the unicode spec + +#### `escape_forward_slash` #### + +json strings are escaped according to the json spec. this means forward slashes are never escaped. unfortunately, a microsoft implementation of json uses escaped forward slashes in json formatted date strings. without this option it is impossible to get date strings that some microsoft tools understand + +#### `explicit_end` #### + +this option treats all exhausted inputs as incomplete, as explained below. the parser will not attempt to return a final state until the function is called with the value `end_stream` + +#### `single_quotes` #### + +some parsers allow double quotes (`u+0022`) to be replaced by single quotes (`u+0027`) to deliminate keys and strings. this option allows json containing single quotes as structural (deliminator) characters to be parsed without errors. note that the parser expects strings to be terminated by the same quote type that opened it and that single quotes must, obviously, be escaped within strings deliminated by single quotes. the parser will never emit json with keys or strings deliminated by single quotes + +#### `no_jsonp_escapes` #### + +javascript interpreters treat the codepoints `u+2028` and `u+2029` as significant whitespace. json strings that contain either of these codepoints will be parsed incorrectly by some javascript interpreters. by default, these codepoints are escaped (to `"\u2028"` and `\u2029`, respectively) to retain compatibility. this option simply removes that escaping if, for some reason, you object to this + +#### `comments` #### + +json has no official comments but some parsers allow c style comments. this flag allows comments (both `// ...` and `/* ... */` style) anywhere whitespace is allowed + + +### incomplete input ### + +jsx handles incomplete json texts. if a partial json text is parsed, rather than returning a term from your callback handler, jsx returns `{incomplete, F}` where `F` is a function with an identical API to the anonymous fun returned from `decoder/3`. it retains the internal state of the parser at the point where input was exhausted. this allows you to parse as you stream json over a socket or file descriptor or to parse large json texts without needing to keep them entirely in memory + +however, it is important to recognize that jsx is greedy by default. if input is exhausted and the json text is not unambiguously incomplete jsx will consider the parsing complete. this is mostly relevant when parsing bare numbers like `<<"1234">>`. this could be a complete json integer or just the beginning of a json integer that is being parsed incrementally. jsx will treat it as a whole integer. the option `explicit_end` can be used to modify this behaviour, see above + + +### the encoder and decoder ### + +jsx is built on top of two finite state automata, one that handles json texts and one that handles erlang terms. both take a callback module as an argument that acts similar to a fold over a list of json 'events'. these events and the handler module's callbacks are detailed in the next section + +`jsx:decoder/3` and `jsx:encoder/3` are the entry points for the decoder and encoder, respectively + +`decoder(Handler, InitialState, Opts)` -> `Fun((JSON) -> Any)` +`encoder(Handler, InitialState, Opts)` -> `Fun((Term) -> Any)` + +types: + +- `Handler` = `atom()`, should be the name of a callback module, see below +- `InitialState` = `term()`, passed as is to `Handler:init/1` +- `Opts` = see above +- `JSON` = `utf8` encoded json text +- `Term` = an erlang term as specified above in the mapping section +- `Any` = `term()` + +decoder returns an anonymous function that handles binary json input and encoder returns an anonymous function that handles erlang term input. these are safe to reuse for multiple inputs + + +### handler callbacks ### + +`Handler` should export the following pair of functions + +`Handler:init(InitialState)` -> `State` +`Handler:handle_event(Event, State)` -> `NewState` + +types: + +- `InitialState`, `State`, `NewState` = any erlang term +- `Event` = + * `start_object` + * `end_object` + * `start_array` + * `end_array` + * `end_json` + * `{key, binary()}` + * `{string, binary()}` + * `{integer, integer()}` + * `{float, float()}` + * `{literal, true}` + * `{literal, false}` + * `{literal, null}` + +`init/1` is called with the `initialState` argument from `decoder/3` or `encoder/3` and should take care of any initialization your handler requires and return a new state + +`handle_event/2` is called for each `Event` emitted by the decoder/encoder with the output of the previous `handle_event/2` call (or `init/1` call, if `handle_event/2` has not yet been called) + +the event `end_json` will always be the last event emitted, you should take care of any cleanup in `handle_event/2` when encountering `end_json`. the state returned from this call will be returned as the final result of the anonymous function + +both `key` and `string` are `utf8` encoded binaries with all escaped values converted into the appropriate codepoints + + +### converting json to erlang and vice versa ### + +#### converting json to erlang terms #### + +`to_term` parses a JSON text (a utf8 encoded binary) and produces an erlang term (see json <-> erlang mapping details above) `to_term(JSON)` -> `Term` - `to_term(JSON, Opts)` -> `Term` types: -* `JSON` = `binary()` -* `Term` = `[]` | `[{}]` | `[Value]` | `[{Label, Value}]` | `{incomplete, Fun}` -* `Value` = `binary()` | `integer()` | `float()` | `true` | `false` | `null` -* `Label` = `binary()` | `atom()` -* `Fun` = `fun(JSON)` -> `Term` -* `Opts` = `[]` | `[Opt]` +* `JSON` = as above in the mapping section +* `Term` = as above in the mapping section +* `Opts` = as above in the opts section, but see also additional opts below * `Opt` = - - `loose_unicode` - - `single_quotes` - `labels` - `{labels, Label}` - `Label` = * `binary` * `atom` * `existing_atom` - - `explicit_end` - -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors - -valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `labels` controls how keys are converted from json to erlang terms. `binary` does no conversion beyond normal escaping. `atom` converts keys to erlang atoms, and results in a badarg error if keys fall outside the range of erlang atoms. `existing_atom` is identical to `atom`, except it will not add new atoms to the atom table -see the note below about streaming mode for details of `explicit_end` - -**converting erlang terms to json** +#### converting erlang terms to json #### -produces a JSON text from an erlang term (see json <-> erlang mapping details below) +`to_json` parses an erlang term and produces a JSON text (see json <-> erlang mapping details below) `to_json(Term)` -> `JSON` - `to_json(Term, Opts)` -> `JSON` types: -* `JSON` = `binary()` -* `Term` = `[]` | `[{}]` | `[Value]` | `[{Label, Value}]` | `{incomplete, Fun}` -* `Value` = `binary()` | `integer()` | `float()` | `true` | `false` | `null` -* `Label` = `binary()` | `atom()` -* `Opts` = `[]` | `[Opt]` +* `JSON` = as above in the mapping section +* `Term` = as above in the mapping section +* `Opts` = as above in the opts section, but see also additional opts below * `Opt` = - `space` - `{space, N}` - `indent` - `{indent, N}` - - `escape_forward_slash` the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` the option `{indent, N}` inserts a newline and `N` spaces for each level of indentation in your json output. note that this overrides spaces inserted after a comma. `indent` is an alias for `{indent, 1}`. the default is `{indent, 0}` - -if the option `escape_forward_slash` is enabled, `$/` is escaped. this is not normally required but is necessary for compatibility with microsoft's json date format -**formatting json texts** +### formatting and minifying json text ### + +#### formatting json texts #### produces a JSON text from JSON text, reformatted `format(JSON)` -> `JSON` - `format(JSON, Opts)` -> `JSON` types: -* `JSON` = `binary()` -* `Term` = `[]` | `[{}]` | `[Value]` | `[{Label, Value}]` | `{incomplete, Fun}` -* `Value` = `binary()` | `integer()` | `float()` | `true` | `false` | `null` -* `Label` = `binary()` | `atom()` -* `Fun` = `fun(JSON)` -> `Term` -* `Opts` = `[]` | `[Opt]` +* `JSON` = as above in the mapping section +* `Opts` = as above in the opts section, but see also additional opts below * `Opt` = - `space` - `{space, N}` - `indent` - `{indent, N}` - - `loose_unicode` - - `single_quotes` - - `escape_forward_slash` - - `explicit_end` - -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors - -valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` the option `{indent, N}` inserts a newline and `N` spaces for each level of indentation in your json output. note that this overrides spaces inserted after a comma. `indent` is an alias for `{indent, 1}`. the default is `{indent, 0}` -if the option `escape_forward_slash` is enabled, `$/` is escaped. this is not normally required but is necessary for compatibility with microsoft's json date format - -see the note below about streaming mode for details of `explicit_end` +calling `format` with no options results in minified json text -**verifying json texts** +### verifying json and terms are valid input ### + +#### verifying json texts #### returns true if input is a valid JSON text, false if not `is_json(MaybeJSON)` -> `true` | `false` | `{incomplete, Fun}` - `is_json(MaybeJSON, Opts)` -> `true` | `false` | `{incomplete, Fun}` types: * `MaybeJSON` = `any()` -* `Opts` = `[]` | `[Opt]` -* `Opt` = - - `loose_unicode` - - `single_quotes` - - `explicit_end` - -see `json_to_term` for details of options +* `Opts` = as above -**verifying json texts** +#### verifying terms #### returns true if input is a valid erlang term that represents a JSON text, false if not `is_term(MaybeJSON)` -> `true` | `false` +`is_term(MaybeJSON, Opts)` -> `true` | `false` types: * `MaybeJSON` = `any()` +* `Opts` = as above -**streaming mode** - -this implementation is interruptable and reentrant and may be used to incrementally parse json texts. it's greedy and will exhaust input, returning when the stream buffer is empty. if the json text is so far valid, but incomplete (or if the option `explicit_end` has been selected), `{incomplete, Fun}` will be returned. `Fun/1` may be called with additional input (or the atom `end_stream` to force the end of parsing) - -`explicit_end` is of use when parsing bare numbers (like `123` or `-0.987` for example) as they may have no unambiguous end when encountered in a stream. it is also of use when reading from a socket or file and there may be unprocessed white space (or errors) left in the stream - - -## json <-> erlang ## - -**json** | **erlang** ---------------------------------|-------------------------------- -`number` | `integer()` OR `float()` -`string` | `binary()` -`true`, `false` and `null` | `true`, `false` and `null` -`array` | `[]` OR `[JSON]` -`object` | `[{}]` OR `[{binary(), JSON}]` - -**json** - -json must be encoded in `utf8`. if it's invalid `utf8`, it probably won't parse without errors. one optional exception is made for json strings that are otherwise `utf8`, see under `strings` below. - -**numbers** - -javascript and thus json represent all numeric values with floats. as this is woefully insufficient for many uses, **jsx**, just like erlang, supports bigints. whenever possible, this library will interpret json numbers that look like integers as integers. other numbers will be converted to erlang's floating point type, which is nearly but not quite iee754. negative zero is not representable in erlang (zero is unsigned in erlang and `0` is equivalent to `-0`) and will be interpreted as regular zero. numbers not representable are beyond the concern of this implementation, and will result in parsing errors - -when converting from erlang to json, numbers are represented with their shortest representation that will round trip without loss of precision. this means that some floats may be superficially dissimilar (although functionally equivalent). for example, `1.0000000000000001` will be represented by `1.0` - -**strings** - -the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters and quotes) and that are encoded in `utf8`. in the interests of pragmatism, however, the parser option `loose_unicode` attempts to replace invalid `utf8` sequences with the replacement codepoint `u+fffd` when possible - -all erlang strings are represented by *valid* `utf8` encoded binaries - -this implementation performs no normalization on strings beyond that detailed here. be careful when comparing strings as equivalent strings may have different `utf8` encodings - -**true, false and null** - -the json primitives `true`, `false` and `null` are represented by the erlang atoms `true`, `false` and `null`. surprise - -**arrays** - -json arrays are represented with erlang lists of json values as described in this document - -**objects** - -json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties. all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers) - - -## acknowledgements ## +## acknowledgements ## paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides and alex kropivny have all contributed to the development of jsx, whether they know it or not +[json]: http://json.org [yajl]: http://lloyd.github.com/yajl [MIT]: http://www.opensource.org/licenses/mit-license.html [rebar]: https://github.com/basho/rebar [meck]: https://github.com/eproxus/meck -[json]: http://json.org [rfc4627]: http://tools.ietf.org/html/rfc4627 \ No newline at end of file From 95e0c20e0d7b31e664ad38a8b9211d76e6fd1abf Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 19:42:03 -0700 Subject: [PATCH 14/75] readme fixes for github's markdown --- README.markdown | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index b8da7de..89aa2d8 100644 --- a/README.markdown +++ b/README.markdown @@ -13,9 +13,9 @@ jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suit ## index ## -* #### [introduction](#intro) #### -* #### [quickstart](#quickstart) #### -* #### [the api](#api) #### +* [introduction](#intro) +* [quickstart](#quickstart) +* [the api](#api) - [json <-> erlang mapping](#mapping) - [options](#options) - [incomplete input](#incompletes) @@ -24,7 +24,7 @@ jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suit - [converting json to erlang and vice versa](#convert) - [formatting and minifying json text](#format) - [verifying json and terms are valid input](#verify) - * #### [acknowledgments](#thanks) #### + * [acknowledgments](#thanks) @@ -136,6 +136,7 @@ jsx is built on top of two finite state automata, one that handles json texts an `jsx:decoder/3` and `jsx:encoder/3` are the entry points for the decoder and encoder, respectively `decoder(Handler, InitialState, Opts)` -> `Fun((JSON) -> Any)` + `encoder(Handler, InitialState, Opts)` -> `Fun((Term) -> Any)` types: @@ -155,6 +156,7 @@ decoder returns an anonymous function that handles binary json input and encoder `Handler` should export the following pair of functions `Handler:init(InitialState)` -> `State` + `Handler:handle_event(Event, State)` -> `NewState` types: @@ -190,6 +192,7 @@ both `key` and `string` are `utf8` encoded binaries with all escaped values conv `to_term` parses a JSON text (a utf8 encoded binary) and produces an erlang term (see json <-> erlang mapping details above) `to_term(JSON)` -> `Term` + `to_term(JSON, Opts)` -> `Term` types: @@ -212,6 +215,7 @@ the option `labels` controls how keys are converted from json to erlang terms. ` `to_json` parses an erlang term and produces a JSON text (see json <-> erlang mapping details below) `to_json(Term)` -> `JSON` + `to_json(Term, Opts)` -> `JSON` types: @@ -237,6 +241,7 @@ the option `{indent, N}` inserts a newline and `N` spaces for each level of inde produces a JSON text from JSON text, reformatted `format(JSON)` -> `JSON` + `format(JSON, Opts)` -> `JSON` types: @@ -263,6 +268,7 @@ calling `format` with no options results in minified json text returns true if input is a valid JSON text, false if not `is_json(MaybeJSON)` -> `true` | `false` | `{incomplete, Fun}` + `is_json(MaybeJSON, Opts)` -> `true` | `false` | `{incomplete, Fun}` types: @@ -276,6 +282,7 @@ types: returns true if input is a valid erlang term that represents a JSON text, false if not `is_term(MaybeJSON)` -> `true` | `false` + `is_term(MaybeJSON, Opts)` -> `true` | `false` types: From 1028a229c5df6bd123014a661e90cdf59e891eb7 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 22:42:58 -0700 Subject: [PATCH 15/75] minor fixes for illegal utf8 sequences and better testing thereof --- src/jsx_encoder.erl | 130 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 14 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index f97bf02..9e362ee 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -118,7 +118,8 @@ check_string(<>) when C < 16#fdd0 -> check_string(<>) when C > 16#fdef, C < 16#fffe -> check_string(Rest); check_string(<>) - when C =/= 16#fffe andalso C =/= 16#ffff andalso + when C > 16#fffd andalso + C =/= 16#fffe andalso C =/= 16#ffff andalso C =/= 16#1fffe andalso C =/= 16#1ffff andalso C =/= 16#2fffe andalso C =/= 16#2ffff andalso C =/= 16#3fffe andalso C =/= 16#3ffff andalso @@ -140,7 +141,6 @@ check_string(<<>>) -> true; check_string(<<_, _/binary>>) -> false. clean_string(<>, Acc) when C >= 16#fdd0, C =< 16#fdef -> - io:format("1: ~p~n", [C]), clean_string(Rest, <>); clean_string(<>, Acc) when C == 16#fffe orelse C == 16#ffff orelse @@ -160,11 +160,13 @@ clean_string(<>, Acc) C == 16#efffe orelse C == 16#effff orelse C == 16#ffffe orelse C == 16#fffff orelse C == 16#10fffe orelse C == 16#10ffff -> - io:format("2: ~p~n", [C]), clean_string(Rest, <>); clean_string(<>, Acc) -> - io:format("3: ~p~n", [C]), clean_string(Rest, <>); +clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> + clean_string(Rest, <>); +clean_string(<<_, Rest/binary>>, Acc) -> + clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. @@ -174,7 +176,10 @@ clean_string(<<>>, Acc) -> Acc. encode(Term) -> (encoder(jsx, [], []))(Term). -encode(Term, Opts) -> (encoder(jsx, [], Opts))(Term). +encode(Term, Opts) -> + try (encoder(jsx, [], Opts))(Term) + catch _:_ -> {error, badjson} + end. encode_test_() -> @@ -245,17 +250,114 @@ encode_test_() -> encode([{key, <<"value">>}]), [start_object, {key, <<"key">>}, {string, <<"value">>}, end_object, end_json] ) + } + ]. + +noncharacters_test_() -> + [ + {"noncharacters - badjson", + ?_assertEqual(check_bad(noncharacters()), []) }, - {"bad string", ?_assertError( - badarg, - encode([<<"a bad string: ", 16#ffff/utf8>>]) - ) - }, - {"allow bad string", ?_assertEqual( - encode([<<"a bad string: ", 16#1ffff/utf8>>], [loose_unicode]), - [start_array, {string, <<"a bad string: ", 16#fffd/utf8>>}, end_array, end_json] - ) + {"noncharacters - replaced", + ?_assertEqual(check_replaced(noncharacters()), []) } ]. +extended_noncharacters_test_() -> + [ + {"extended noncharacters - badjson", + ?_assertEqual(check_bad(extended_noncharacters()), []) + }, + {"extended noncharacters - replaced", + ?_assertEqual(check_replaced(extended_noncharacters()), []) + } + ]. + +surrogates_test_() -> + [ + {"surrogates - badjson", + ?_assertEqual(check_bad(surrogates()), []) + }, + {"surrogates - replaced", + ?_assertEqual(check_replaced(surrogates()), []) + } + ]. + +reserved_test_() -> + [ + {"reserved noncharacters - badjson", + ?_assertEqual(check_bad(reserved_space()), []) + }, + {"reserved noncharacters - replaced", + ?_assertEqual(check_replaced(reserved_space()), []) + } + ]. + +good_characters_test_() -> + [ + {"acceptable codepoints", + ?_assertEqual(check_good(good()), []) + }, + {"acceptable extended", + ?_assertEqual(check_good(good_extended()), []) + } + ]. + + +check_bad(List) -> + lists:dropwhile(fun({_, {error, badjson}}) -> true ; (_) -> false end, + check(List, [], []) + ). + +check_replaced(List) -> + lists:dropwhile(fun({_, [{string, <<16#fffd/utf8>>}|_]}) -> true + ; (_) -> false + end, + check(List, [loose_unicode], []) + ). + +check_good(List) -> + lists:dropwhile(fun({_, [{string, _}|_]}) -> true ; (_) -> false end, + check(List, [], []) + ). + +check([], _Opts, Acc) -> Acc; +check([H|T], Opts, Acc) -> + R = encode(to_fake_utf(H, utf8), Opts), + check(T, Opts, [{H, R}] ++ Acc). + + + +noncharacters() -> lists:seq(16#fffe, 16#ffff). + +extended_noncharacters() -> + [16#1fffe, 16#1ffff, 16#2fffe, 16#2ffff] + ++ [16#3fffe, 16#3ffff, 16#4fffe, 16#4ffff] + ++ [16#5fffe, 16#5ffff, 16#6fffe, 16#6ffff] + ++ [16#7fffe, 16#7ffff, 16#8fffe, 16#8ffff] + ++ [16#9fffe, 16#9ffff, 16#afffe, 16#affff] + ++ [16#bfffe, 16#bffff, 16#cfffe, 16#cffff] + ++ [16#dfffe, 16#dffff, 16#efffe, 16#effff] + ++ [16#ffffe, 16#fffff, 16#10fffe, 16#10ffff]. + +surrogates() -> lists:seq(16#d800, 16#dfff). + +reserved_space() -> lists:seq(16#fdd0, 16#fdef). + +good() -> lists:seq(1, 16#d7ff) ++ lists:seq(16#e000, 16#fdcf) ++ lists:seq(16#fdf0, 16#fffd). + +good_extended() -> lists:seq(16#100000, 16#10fffd). + +%% erlang refuses to encode certain codepoints, so fake them all +to_fake_utf(N, utf8) when N < 16#0080 -> <>; +to_fake_utf(N, utf8) when N < 16#0800 -> + <<0:5, Y:5, X:6>> = <>, + <<2#110:3, Y:5, 2#10:2, X:6>>; +to_fake_utf(N, utf8) when N < 16#10000 -> + <> = <>, + <<2#1110:4, Z:4, 2#10:2, Y:6, 2#10:2, X:6>>; +to_fake_utf(N, utf8) -> + <<0:3, W:3, Z:6, Y:6, X:6>> = <>, + <<2#11110:5, W:3, 2#10:2, Z:6, 2#10:2, Y:6, 2#10:2, X:6>>. + -endif. \ No newline at end of file From aa919ce50048080f6f38946f95e2b127a80bb8a9 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 22:47:16 -0700 Subject: [PATCH 16/75] minor fixes for illegal utf8 sequences and better testing thereof --- src/jsx_encoder.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 9e362ee..a2c5bbc 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -161,10 +161,10 @@ clean_string(<>, Acc) C == 16#ffffe orelse C == 16#fffff orelse C == 16#10fffe orelse C == 16#10ffff -> clean_string(Rest, <>); -clean_string(<>, Acc) -> - clean_string(Rest, <>); clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); +clean_string(<>, Acc) -> + clean_string(Rest, <>); clean_string(<<_, Rest/binary>>, Acc) -> clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. From 9d4edd6c4d48aa972992e873241292f01a68a428 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 22:53:18 -0700 Subject: [PATCH 17/75] fix for older erts versions where the private space reserved characters are not recognized --- src/jsx_encoder.erl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 9e362ee..9c10a19 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -161,10 +161,12 @@ clean_string(<>, Acc) C == 16#ffffe orelse C == 16#fffff orelse C == 16#10fffe orelse C == 16#10ffff -> clean_string(Rest, <>); -clean_string(<>, Acc) -> - clean_string(Rest, <>); clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); +clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> + clean_string(Rest, <>); +clean_string(<>, Acc) -> + clean_string(Rest, <>); clean_string(<<_, Rest/binary>>, Acc) -> clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. From 66add159b5c4113903a3565257b1f8c4ade1ed01 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:00:33 -0700 Subject: [PATCH 18/75] fix for older erts that don't allow noncharacters --- src/jsx_encoder.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 9c10a19..21e5269 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -165,6 +165,8 @@ clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> clean_string(Rest, <>); +clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190, X == 191 -> + clean_string(Rest, <>); clean_string(<>, Acc) -> clean_string(Rest, <>); clean_string(<<_, Rest/binary>>, Acc) -> From b406afaa779566ce685942b6e2291e230cd8cc3d Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:07:13 -0700 Subject: [PATCH 19/75] remove rogue function head --- src/jsx_encoder.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 21e5269..6f993f1 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -140,8 +140,6 @@ check_string(<>) check_string(<<>>) -> true; check_string(<<_, _/binary>>) -> false. -clean_string(<>, Acc) when C >= 16#fdd0, C =< 16#fdef -> - clean_string(Rest, <>); clean_string(<>, Acc) when C == 16#fffe orelse C == 16#ffff orelse C == 16#1fffe orelse C == 16#1ffff orelse @@ -161,10 +159,13 @@ clean_string(<>, Acc) C == 16#ffffe orelse C == 16#fffff orelse C == 16#10fffe orelse C == 16#10ffff -> clean_string(Rest, <>); +%% surrogates clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); +%% private use noncharacters clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> clean_string(Rest, <>); +%% u+fffe and u+ffff (required for otp < r15) clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190, X == 191 -> clean_string(Rest, <>); clean_string(<>, Acc) -> From 07c1f5716c8071130624bc08209c9ed96a53e2f0 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:13:27 -0700 Subject: [PATCH 20/75] finally found actual cause of otp r14x bug --- src/jsx_encoder.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 6f993f1..35c0af2 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -118,7 +118,7 @@ check_string(<>) when C < 16#fdd0 -> check_string(<>) when C > 16#fdef, C < 16#fffe -> check_string(Rest); check_string(<>) - when C > 16#fffd andalso + when C > 16#ffff andalso C =/= 16#fffe andalso C =/= 16#ffff andalso C =/= 16#1fffe andalso C =/= 16#1ffff andalso C =/= 16#2fffe andalso C =/= 16#2ffff andalso From f8f436e0a0527348a2bc33bfcd1101c8e89f2a22 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:17:57 -0700 Subject: [PATCH 21/75] ok, now it's fixed for older releases --- src/jsx_encoder.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 35c0af2..f6c0c2f 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -24,6 +24,7 @@ -module(jsx_encoder). -export([encoder/3]). +-compile(export_all). -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). @@ -118,8 +119,7 @@ check_string(<>) when C < 16#fdd0 -> check_string(<>) when C > 16#fdef, C < 16#fffe -> check_string(Rest); check_string(<>) - when C > 16#ffff andalso - C =/= 16#fffe andalso C =/= 16#ffff andalso + when C > 16#ffff andalso C =/= 16#1fffe andalso C =/= 16#1ffff andalso C =/= 16#2fffe andalso C =/= 16#2ffff andalso C =/= 16#3fffe andalso C =/= 16#3ffff andalso From 0b789147a59053e2439685875746c39b4adaaf14 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:18:39 -0700 Subject: [PATCH 22/75] remove export_all flag --- src/jsx_encoder.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index f6c0c2f..1308a6e 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -24,7 +24,6 @@ -module(jsx_encoder). -export([encoder/3]). --compile(export_all). -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). From c9ea2975bd10038209e80ac0d52cd4c58029ad01 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:58:22 -0700 Subject: [PATCH 23/75] whitelist allowed codepoints rather than blacklist disallowed codepoints in jsx_encoder --- src/jsx_encoder.erl | 56 +++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 1308a6e..4ee0fa5 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -139,37 +139,33 @@ check_string(<>) check_string(<<>>) -> true; check_string(<<_, _/binary>>) -> false. -clean_string(<>, Acc) - when C == 16#fffe orelse C == 16#ffff orelse - C == 16#1fffe orelse C == 16#1ffff orelse - C == 16#2fffe orelse C == 16#2ffff orelse - C == 16#3fffe orelse C == 16#3ffff orelse - C == 16#4fffe orelse C == 16#4ffff orelse - C == 16#5fffe orelse C == 16#5ffff orelse - C == 16#6fffe orelse C == 16#6ffff orelse - C == 16#7fffe orelse C == 16#7ffff orelse - C == 16#8fffe orelse C == 16#8ffff orelse - C == 16#9fffe orelse C == 16#9ffff orelse - C == 16#afffe orelse C == 16#affff orelse - C == 16#bfffe orelse C == 16#bffff orelse - C == 16#cfffe orelse C == 16#cffff orelse - C == 16#dfffe orelse C == 16#dffff orelse - C == 16#efffe orelse C == 16#effff orelse - C == 16#ffffe orelse C == 16#fffff orelse - C == 16#10fffe orelse C == 16#10ffff -> - clean_string(Rest, <>); -%% surrogates -clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> - clean_string(Rest, <>); -%% private use noncharacters -clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> - clean_string(Rest, <>); -%% u+fffe and u+ffff (required for otp < r15) -clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190, X == 191 -> - clean_string(Rest, <>); -clean_string(<>, Acc) -> + +clean_string(<>, Acc) when C < 16#fdd0 -> clean_string(Rest, <>); -clean_string(<<_, Rest/binary>>, Acc) -> +clean_string(<>, Acc) when C > 16#fdef, C < 16#fffe -> + clean_string(Rest, <>); +clean_string(<>, Acc) + when C > 16#ffff andalso + C =/= 16#1fffe andalso C =/= 16#1ffff andalso + C =/= 16#2fffe andalso C =/= 16#2ffff andalso + C =/= 16#3fffe andalso C =/= 16#3ffff andalso + C =/= 16#4fffe andalso C =/= 16#4ffff andalso + C =/= 16#5fffe andalso C =/= 16#5ffff andalso + C =/= 16#6fffe andalso C =/= 16#6ffff andalso + C =/= 16#7fffe andalso C =/= 16#7ffff andalso + C =/= 16#8fffe andalso C =/= 16#8ffff andalso + C =/= 16#9fffe andalso C =/= 16#9ffff andalso + C =/= 16#afffe andalso C =/= 16#affff andalso + C =/= 16#bfffe andalso C =/= 16#bffff andalso + C =/= 16#cfffe andalso C =/= 16#cffff andalso + C =/= 16#dfffe andalso C =/= 16#dffff andalso + C =/= 16#efffe andalso C =/= 16#effff andalso + C =/= 16#ffffe andalso C =/= 16#fffff andalso + C =/= 16#10fffe andalso C =/= 16#10ffff -> + clean_string(Rest, <>); +clean_string(<>, Acc) when X == 237; X == 239 -> + clean_string(Rest, <>); +clean_string(<<_, _, _, _, Rest/binary>>, Acc) -> clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. From e103ddbed26c53bcfad36ab0be93875834a0cba3 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 00:00:34 -0700 Subject: [PATCH 24/75] typo in readme --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 89aa2d8..5256175 100644 --- a/README.markdown +++ b/README.markdown @@ -24,7 +24,7 @@ jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suit - [converting json to erlang and vice versa](#convert) - [formatting and minifying json text](#format) - [verifying json and terms are valid input](#verify) - * [acknowledgments](#thanks) +* [acknowledgments](#thanks) From 145368708008bd84973ed13839ca0d86a270c6e9 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 04:34:33 -0700 Subject: [PATCH 25/75] readme updates and clarifications --- README.markdown | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index 5256175..81bb529 100644 --- a/README.markdown +++ b/README.markdown @@ -74,9 +74,17 @@ when converting from erlang to json, numbers are represented with their shortest #### strings #### -the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters and quotes) and that are encoded in `utf8`. in the interests of pragmatism, there is an option for looser parsing, see options below +the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters, `"` and the escape character, `\`) and that are encoded in `utf8` -all erlang strings are represented by *valid* `utf8` encoded binaries +this means some codepoints that are allowed in javascript strings are not accepted by the parser. the noncharacters are specifically disallowed. the range `u+fdd0` to `u+fdef` is reserved for internal implementation use by the unicode standard and codepoints of the form `u+Xfffe` and `u+Xffff` are reserved for error detection. strings containing these codepoints are generally assumed to be invalid or improper + +also disallowed are improperly paired surrogates. `u+d800` to `u+dfff` are allowed, but only when they form valid surrogate pairs. surrogates that appear otherwise are an error + +json string escapes of the form `\uXXXX` will be converted to their equivalent codepoint during parsing. this means control characters and other codepoints disallowed by the json spec may be encountered in resulting strings, but codepoints disallowed by the unicode spec (like the two cases above) will not be + +in the interests of pragmatism, there is an option for looser parsing, see options below + +all erlang strings are represented by *valid* `utf8` encoded binaries. the encoder will check strings for conformance. the same restrictions apply as for strings encountered within json texts. that means no unpaired surrogates and no non-characters this implementation performs no normalization on strings beyond that detailed here. be careful when comparing strings as equivalent strings may have different `utf8` encodings @@ -90,7 +98,7 @@ json arrays are represented with erlang lists of json values as described in thi #### objects #### -json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties. all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers) +json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties so all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers). values should be valid json values ### options ### @@ -99,7 +107,7 @@ jsx functions all take a common set of options. not all flags have meaning in al #### `loose_unicode` #### -json text input and json strings SHOULD be utf8 encoded binaries, appropriately escaped as per the json spec. if this option is present attempts are made to replace invalid codepoints with `u+FFFD` as per the unicode spec +json text input and json strings SHOULD be utf8 encoded binaries, appropriately escaped as per the json spec. if this option is present attempts are made to replace invalid codepoints with `u+FFFD` as per the unicode spec. this applies both to malformed unicode and disallowed codepoints #### `escape_forward_slash` #### From be89f5f395c0d8e65920b91556b1b9988ace98f4 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 05:19:47 -0700 Subject: [PATCH 26/75] corrected handling of malformed utf8 sequences --- src/jsx_decoder.erl | 61 ++++++++++++++++++++++++++++++++++++++++++++ src/jsx_encoder.erl | 62 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 4055b2e..f70e0f8 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -334,6 +334,20 @@ noncharacter(<<239, 191, X, Rest/binary>>, Handler, [Acc|Stack], Opts) %% surrogates noncharacter(<<237, X, _, Rest/binary>>, Handler, [Acc|Stack], Opts) when X >= 160 -> string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); +noncharacter(<>, Handler, [Acc|Stack], Opts) + when ( + (X == 240 andalso Y == 159) orelse + (X == 240 andalso Y == 175) orelse + (X == 240 andalso Y == 191) orelse + ( + (X == 241 orelse X == 242 orelse X == 243) andalso + (Y == 143 orelse Y == 159 orelse Y == 175 orelse Y == 191) + ) orelse + (X == 244 andalso Y == 143) + ) andalso (Z == 190 orelse Z == 191) -> + string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); +noncharacter(<<_, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); noncharacter(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). @@ -1079,6 +1093,51 @@ good_characters_test_() -> ?_assertEqual(check_good(good_extended()), []) } ]. + +malformed_test_() -> + [ + {"malformed codepoint with 1 byte", + ?_assertEqual({error, badjson}, decode(<<128>>)) + }, + {"malformed codepoint with 2 bytes", + ?_assertEqual({error, badjson}, decode(<<128, 192>>)) + }, + {"malformed codepoint with 3 bytes", + ?_assertEqual({error, badjson}, decode(<<128, 192, 192>>)) + }, + {"malformed codepoint with 4 bytes", + ?_assertEqual({error, badjson}, decode(<<128, 192, 192, 192>>)) + } + ]. + +malformed_replaced_test_() -> + F = <<16#fffd/utf8>>, + [ + {"malformed codepoint with 1 byte", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 34>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 2 bytes", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 192, 34>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 3 bytes", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 192, 192, 34>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 4 bytes", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 192, 192, 192, 34>>, [loose_unicode]) + ) + } + ]. check_bad(List) -> @@ -1104,6 +1163,8 @@ check([H|T], Opts, Acc) -> check(T, Opts, [{H, R}] ++ Acc). +decode(JSON) -> decode(JSON, []). + decode(JSON, Opts) -> try (decoder(jsx, [], Opts))(JSON) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 4ee0fa5..5cd9934 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -163,9 +163,29 @@ clean_string(<>, Acc) C =/= 16#ffffe andalso C =/= 16#fffff andalso C =/= 16#10fffe andalso C =/= 16#10ffff -> clean_string(Rest, <>); -clean_string(<>, Acc) when X == 237; X == 239 -> +%% surrogates +clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); -clean_string(<<_, _, _, _, Rest/binary>>, Acc) -> +%% private use noncharacters +clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 143, X =< 175 -> + clean_string(Rest, <>); +%% u+fffe and u+ffff +clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190; X == 191 -> + clean_string(Rest, <>); +%% the u+Xfffe and u+Xffff noncharacters +clean_string(<>, Acc) + when ( + (X == 240 andalso Y == 159) orelse + (X == 240 andalso Y == 175) orelse + (X == 240 andalso Y == 191) orelse + ( + (X == 241 orelse X == 242 orelse X == 243) andalso + (Y == 143 orelse Y == 159 orelse Y == 175 orelse Y == 191) + ) orelse + (X == 244 andalso Y == 143) + ) andalso (Z == 190 orelse Z == 191) -> + clean_string(Rest, <>); +clean_string(<<_, Rest/binary>>, Acc) -> clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. @@ -302,7 +322,43 @@ good_characters_test_() -> ?_assertEqual(check_good(good_extended()), []) } ]. - + +malformed_test_() -> + [ + {"malformed codepoint with 1 byte", ?_assertError(badarg, encode(<<128>>))}, + {"malformed codepoint with 2 bytes", ?_assertError(badarg, encode(<<128, 192>>))}, + {"malformed codepoint with 3 bytes", ?_assertError(badarg, encode(<<128, 192, 192>>))}, + {"malformed codepoint with 4 bytes", ?_assertError(badarg, encode(<<128, 192, 192, 192>>))} + ]. + +malformed_replaced_test_() -> + F = <<16#fffd/utf8>>, + [ + {"malformed codepoint with 1 byte", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 2 bytes", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128, 192>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 3 bytes", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128, 192, 192>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 4 bytes", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128, 192, 192, 192>>, [loose_unicode]) + ) + } + ]. check_bad(List) -> lists:dropwhile(fun({_, {error, badjson}}) -> true ; (_) -> false end, From 46767102e1c8c1982441a57f8361e7e2f0d771d4 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 05:20:41 -0700 Subject: [PATCH 27/75] freezing for 1.1rc --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 81bb529..60cf62d 100644 --- a/README.markdown +++ b/README.markdown @@ -1,4 +1,4 @@ -# jsx (v1.0) # +# jsx (v1.1) # a sane [json][json] implementation for erlang, inspired by [yajl][yajl] From 1d5b9e74102f7c1fada793654741cd073165340a Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 14 Mar 2012 23:01:59 -0700 Subject: [PATCH 28/75] the option single_quotes in functions dealing with json inputs now allows json that uses single quotes to deliminate keys and strings to be processed, note that this changes the escaping rules slightly --- README.markdown | 11 +++++++++-- src/jsx.erl | 46 +++++++++++++++++++++++++++++++++++++++++++++ src/jsx_decoder.erl | 39 ++++++++++++++++++++++++++++++-------- src/jsx_opts.hrl | 2 +- src/jsx_utils.erl | 6 ++++-- 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/README.markdown b/README.markdown index 473882f..0464f00 100644 --- a/README.markdown +++ b/README.markdown @@ -32,6 +32,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `labels` - `{labels, Label}` - `Label` = @@ -40,7 +41,9 @@ types: * `existing_atom` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `labels` controls how keys are converted from json to erlang terms. `binary` does no conversion beyond normal escaping. `atom` converts keys to erlang atoms, and results in a badarg error if keys fall outside the range of erlang atoms. `existing_atom` is identical to `atom`, except it will not add new atoms to the atom table @@ -97,10 +100,13 @@ types: - `indent` - `{indent, N}` - `loose_unicode` + - `single_quotes` - `escape_forward_slash` - `explicit_end` -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors +`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors + +valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` @@ -125,6 +131,7 @@ types: * `Opts` = `[]` | `[Opt]` * `Opt` = - `loose_unicode` + - `single_quotes` - `explicit_end` see `json_to_term` for details of options diff --git a/src/jsx.erl b/src/jsx.erl index 6c0dbe5..722f674 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -120,6 +120,52 @@ encoder_decoder_equiv_test_() -> ]. +single_quotes_test_() -> + [ + {"single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true}">>, [single_quotes]), + [{<<"key">>, true}] + ) + }, + {"multiple single quoted keys", + ?_assertEqual( + to_term(<<"{'key':true, 'another key':true}">>, [single_quotes]), + [{<<"key">>, true}, {<<"another key">>, true}] + ) + }, + {"nested single quoted keys", + ?_assertEqual( + to_term(<<"{'key': {'key':true, 'another key':true}}">>, [single_quotes]), + [{<<"key">>, [{<<"key">>, true}, {<<"another key">>, true}]}] + ) + }, + {"single quoted string", + ?_assertEqual( + to_term(<<"['string']">>, [single_quotes]), + [<<"string">>] + ) + }, + {"single quote in double quoted string", + ?_assertEqual( + to_term(<<"[\"a single quote: '\"]">>), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote in single quoted string", + ?_assertEqual( + to_term(<<"['a single quote: \\'']">>, [single_quotes]), + [<<"a single quote: '">>] + ) + }, + {"escaped single quote when single quotes are disallowed", + ?_assertError( + badarg, + to_term(<<"[\"a single quote: \\'\"]">>) + ) + } + ]. + %% test handler init([]) -> []. diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 0a82ea2..700ba0a 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -59,7 +59,8 @@ decoder(Handler, State, Opts) -> %% kv seperator -define(comma, 16#2C). --define(quote, 16#22). +-define(doublequote, 16#22). +-define(singlequote, 16#27). -define(colon, 16#3A). %% string escape sequences @@ -130,7 +131,9 @@ decoder(Handler, State, Opts) -> -define(end_seq(Seq), unicode:characters_to_binary(lists:reverse(Seq))). -value(<>, Handler, Stack, Opts) -> +value(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +value(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); value(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); @@ -156,7 +159,9 @@ value(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -object(<>, Handler, Stack, Opts) -> +object(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +object(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); object(<>, {Handler, State}, [key|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); @@ -168,7 +173,9 @@ object(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -array(<>, Handler, Stack, Opts) -> +array(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +array(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); array(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); @@ -206,7 +213,9 @@ colon(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -key(<>, Handler, Stack, Opts) -> +key(<>, Handler, Stack, Opts) -> + string(Rest, Handler, [?new_seq()|Stack], Opts); +key(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq()|Stack], Opts); key(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> key(Rest, Handler, Stack, Opts); @@ -233,13 +242,25 @@ partial_utf(<>) partial_utf(_) -> false. -string(<>, {Handler, State}, [Acc, key|Stack], Opts) -> +string(<>, {Handler, State}, [Acc, key|Stack], Opts) -> colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts ); -string(<>, {Handler, State}, [Acc|Stack], Opts) -> +string(<>, {Handler, State}, [Acc, key|Stack], Opts = #opts{single_quotes=true}) -> + colon(Rest, + {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, + [key|Stack], + Opts + ); +string(<>, {Handler, State}, [Acc|Stack], Opts) -> + maybe_done(Rest, + {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, + Stack, + Opts + ); +string(<>, {Handler, State}, [Acc|Stack], Opts = #opts{single_quotes=true}) -> maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, @@ -318,8 +339,10 @@ escape(<<$t, Rest/binary>>, Handler, [Acc|Stack], Opts) -> escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); escape(<>, Handler, [Acc|Stack], Opts) - when S =:= ?quote; S =:= ?solidus; S =:= ?rsolidus -> + when S =:= ?doublequote; S =:= ?solidus; S =:= ?rsolidus -> string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts = #opts{single_quotes=true}) -> + string(Rest, Handler, [?acc_seq(Acc, ?singlequote)|Stack], Opts); escape(<<>>, Handler, Stack, Opts) -> ?incomplete(escape, <<>>, Handler, Stack, Opts); escape(Bin, Handler, Stack, Opts) -> diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index d49254b..f60627f 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -2,5 +2,5 @@ loose_unicode = false, escape_forward_slash = false, explicit_end = false, - parser = auto + single_quotes = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 814092c..2c8b160 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -43,6 +43,8 @@ parse_opts([escape_forward_slash|Rest], Opts) -> parse_opts(Rest, Opts#opts{escape_forward_slash=true}); parse_opts([explicit_end|Rest], Opts) -> parse_opts(Rest, Opts#opts{explicit_end=true}); +parse_opts([single_quotes|Rest], Opts) -> + parse_opts(Rest, Opts#opts{single_quotes=true}); parse_opts(_, _) -> {error, badarg}. @@ -52,12 +54,12 @@ extract_opts(Opts) -> extract_parser_opts([], Acc) -> Acc; extract_parser_opts([{K,V}|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [{K,V}] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end; extract_parser_opts([K|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of true -> extract_parser_opts(Rest, [K] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end. From 97a7d295f180c5e0011f5369a45aa124e7ed541a Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 14 Mar 2012 23:01:59 -0700 Subject: [PATCH 29/75] the option single_quotes in functions dealing with json inputs now allows json that uses single quotes to deliminate keys and strings to be processed, note that this changes the escaping rules slightly --- src/jsx.erl | 8 +++++++- src/jsx_decoder.erl | 50 ++++++++++++++++++++------------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/jsx.erl b/src/jsx.erl index 722f674..ff1bc09 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -148,7 +148,7 @@ single_quotes_test_() -> }, {"single quote in double quoted string", ?_assertEqual( - to_term(<<"[\"a single quote: '\"]">>), + to_term(<<"[\"a single quote: '\"]">>, [single_quotes]), [<<"a single quote: '">>] ) }, @@ -163,6 +163,12 @@ single_quotes_test_() -> badarg, to_term(<<"[\"a single quote: \\'\"]">>) ) + }, + {"mismatched quotes", + ?_assertError( + badarg, + to_term(<<"['mismatched\"]">>, [single_quotes]) + ) } ]. diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 700ba0a..006cc22 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -134,7 +134,7 @@ decoder(Handler, State, Opts) -> value(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); value(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> - string(Rest, Handler, [?new_seq()|Stack], Opts); + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); value(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); value(<<$f, Rest/binary>>, Handler, Stack, Opts) -> @@ -162,7 +162,7 @@ value(Bin, Handler, Stack, Opts) -> object(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); object(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> - string(Rest, Handler, [?new_seq()|Stack], Opts); + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); object(<>, {Handler, State}, [key|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); object(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> @@ -176,7 +176,7 @@ object(Bin, Handler, Stack, Opts) -> array(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); array(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> - string(Rest, Handler, [?new_seq()|Stack], Opts); + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); array(<<$t, Rest/binary>>, Handler, Stack, Opts) -> tr(Rest, Handler, Stack, Opts); array(<<$f, Rest/binary>>, Handler, Stack, Opts) -> @@ -216,7 +216,7 @@ colon(Bin, Handler, Stack, Opts) -> key(<>, Handler, Stack, Opts) -> string(Rest, Handler, [?new_seq()|Stack], Opts); key(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> - string(Rest, Handler, [?new_seq()|Stack], Opts); + string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); key(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> key(Rest, Handler, Stack, Opts); key(<<>>, Handler, Stack, Opts) -> @@ -242,30 +242,24 @@ partial_utf(<>) partial_utf(_) -> false. -string(<>, {Handler, State}, [Acc, key|Stack], Opts) -> - colon(Rest, - {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, - [key|Stack], - Opts - ); -string(<>, {Handler, State}, [Acc, key|Stack], Opts = #opts{single_quotes=true}) -> - colon(Rest, - {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, - [key|Stack], - Opts - ); -string(<>, {Handler, State}, [Acc|Stack], Opts) -> - maybe_done(Rest, - {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, - Stack, - Opts - ); -string(<>, {Handler, State}, [Acc|Stack], Opts = #opts{single_quotes=true}) -> - maybe_done(Rest, - {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, - Stack, - Opts - ); +string(<>, {Handler, State}, S, Opts) -> + case S of + [Acc, key|Stack] -> + colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [_Acc, single_quote|_Stack] -> + ?error([<>, {Handler, State}, S, Opts]); + [Acc|Stack] -> + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts) + end; +string(<>, {Handler, State}, S, Opts = #opts{single_quotes=true}) -> + case S of + [Acc, single_quote, key|Stack] -> + colon(Rest, {Handler, Handler:handle_event({key, ?end_seq(Acc)}, State)}, [key|Stack], Opts); + [Acc, single_quote|Stack] -> + maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts); + [Acc|Stack] -> + string(Rest, {Handler, State}, [?acc_seq(Acc, ?singlequote)|Stack], Opts) + end; string(<>, Handler, Stack, Opts) -> escape(Rest, Handler, Stack, Opts); %% things get dumb here. erlang doesn't properly restrict unicode non-characters From 8dafdb32b3902413f95f1fea59479187f3f66831 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Thu, 15 Mar 2012 22:56:21 -0700 Subject: [PATCH 30/75] escape strings and keys in the encoder --- src/jsx_encoder.erl | 29 ++++++++++++++++++++++------- src/jsx_utils.erl | 6 +++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 68bff84..f4d83e7 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -53,8 +53,8 @@ start(Term, {Handler, State}, Opts) -> Handler:handle_event(end_json, value(Term, {Handler, State}, Opts)). -value(String, {Handler, State}, _Opts) when is_binary(String) -> - Handler:handle_event({string, String}, State); +value(String, {Handler, State}, Opts) when is_binary(String) -> + Handler:handle_event({string, escape(String, {Handler, State}, Opts)}, State); value(Float, {Handler, State}, _Opts) when is_float(Float) -> Handler:handle_event({float, Float}, State); value(Int, {Handler, State}, _Opts) when is_integer(Int) -> @@ -78,9 +78,18 @@ list_or_object(List, {Handler, State}, Opts) -> object([{Key, Value}|Rest], {Handler, State}, Opts) -> - object(Rest, {Handler, - value(Value, {Handler, Handler:handle_event({key, fix_key(Key)}, State)}, Opts) - }, Opts); + object( + Rest, + { + Handler, + value( + Value, + {Handler, Handler:handle_event({key, escape(fix_key(Key), {Handler, State}, Opts)}, State)}, + Opts + ) + }, + Opts + ); object([], {Handler, State}, _Opts) -> Handler:handle_event(end_object, State); object(Term, Handler, Opts) -> ?error([Term, Handler, Opts]). @@ -91,8 +100,14 @@ list([], {Handler, State}, _Opts) -> Handler:handle_event(end_array, State); list(Term, Handler, Opts) -> ?error([Term, Handler, Opts]). -fix_key(Key) when is_binary(Key) -> Key; -fix_key(Key) when is_atom(Key) -> atom_to_binary(Key, utf8). +fix_key(Key) when is_atom(Key) -> fix_key(atom_to_binary(Key, utf8)); +fix_key(Key) when is_binary(Key) -> Key. + + +escape(String, Handler, Opts) -> + try jsx_utils:json_escape(String, Opts) + catch error:badarg -> erlang:error(badarg, [String, Handler, Opts]) + end. -ifdef(TEST). diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 2c8b160..fa6db97 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -97,7 +97,7 @@ json_escape(<<$\t, Rest/binary>>, Opts, Acc) -> json_escape(<>, Opts, Acc) when C >= 0, C < $\s -> json_escape(Rest, Opts, - <> + <> ); %% escape forward slashes -- optionally -- to faciliate microsoft's retarded %% date format @@ -108,7 +108,7 @@ json_escape(<>, Opts, Acc) when C == 16#2028; C == 16#2029 -> json_escape(Rest, Opts, - <> + <> ); %% any other legal codepoint json_escape(<>, Opts, Acc) -> @@ -122,7 +122,7 @@ json_escape(Rest, Opts, Acc) -> %% convert a codepoint to it's \uXXXX equiv. json_escape_sequence(X) -> <> = <>, - [$\\, $u, (to_hex(A)), (to_hex(B)), (to_hex(C)), (to_hex(D))]. + unicode:characters_to_binary([$\\, $u, (to_hex(A)), (to_hex(B)), (to_hex(C)), (to_hex(D))]). to_hex(10) -> $a; From aef1f71690a01613d106ea7f3b04c70d3fb7bee9 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Thu, 15 Mar 2012 23:06:19 -0700 Subject: [PATCH 31/75] add 'no_jsonp_escapes' flag/option to not escape u+2028 and u+2029 --- src/jsx_opts.hrl | 3 ++- src/jsx_utils.erl | 50 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index f60627f..938b8fb 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -2,5 +2,6 @@ loose_unicode = false, escape_forward_slash = false, explicit_end = false, - single_quotes = false + single_quotes = false, + no_jsonp_escapes = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index fa6db97..2ab87e9 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -45,21 +45,33 @@ parse_opts([explicit_end|Rest], Opts) -> parse_opts(Rest, Opts#opts{explicit_end=true}); parse_opts([single_quotes|Rest], Opts) -> parse_opts(Rest, Opts#opts{single_quotes=true}); +parse_opts([no_jsonp_escapes|Rest], Opts) -> + parse_opts(Rest, Opts#opts{no_jsonp_escapes=true}); parse_opts(_, _) -> {error, badarg}. +valid_flags() -> + [ + loose_unicode, + escape_forward_slash, + explicit_end, + single_quotes, + no_jsonp_escapes + ]. + + extract_opts(Opts) -> extract_parser_opts(Opts, []). extract_parser_opts([], Acc) -> Acc; extract_parser_opts([{K,V}|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of + case lists:member(K, valid_flags()) of true -> extract_parser_opts(Rest, [{K,V}] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end; extract_parser_opts([K|Rest], Acc) -> - case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end, single_quotes]) of + case lists:member(K, valid_flags()) of true -> extract_parser_opts(Rest, [K] ++ Acc) ; false -> extract_parser_opts(Rest, Acc) end. @@ -103,6 +115,10 @@ json_escape(<>, Opts, Acc) when C >= 0, C < $\s -> %% date format json_escape(<<$/, Rest/binary>>, Opts=#opts{escape_forward_slash=true}, Acc) -> json_escape(Rest, Opts, <>); +%% skip escaping u+2028 and u+2029 +json_escape(<>, Opts=#opts{no_jsonp_escapes=true}, Acc) + when C == 16#2028; C == 16#2029 -> + json_escape(Rest, Opts, <>); %% escape u+2028 and u+2029 to avoid problems with jsonp json_escape(<>, Opts, Acc) when C == 16#2028; C == 16#2029 -> @@ -143,27 +159,33 @@ to_hex(X) -> X + 48. %% ascii "1" is [49], "2" is [50], etc... binary_escape_test_() -> [ {"json string escaping", - ?_assert(json_escape( - <<"\"\\\b\f\n\r\t">>, #opts{} - ) =:= <<"\\\"\\\\\\b\\f\\n\\r\\t">> + ?_assertEqual( + json_escape(<<"\"\\\b\f\n\r\t">>, #opts{}), + <<"\\\"\\\\\\b\\f\\n\\r\\t">> ) }, {"json string hex escape", - ?_assert(json_escape( - <<1, 2, 3, 11, 26, 30, 31>>, #opts{} - ) =:= <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> + ?_assertEqual( + json_escape(<<1, 2, 3, 11, 26, 30, 31>>, #opts{}), + <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> ) }, {"jsonp protection", - ?_assert(json_escape( - <<226, 128, 168, 226, 128, 169>>, #opts{} - ) =:= <<"\\u2028\\u2029">> + ?_assertEqual( + json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{}), + <<"\\u2028\\u2029">> + ) + }, + {"no jsonp escapes", + ?_assertEqual( + json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{no_jsonp_escapes=true}), + <<226, 128, 168, 226, 128, 169>> ) }, {"microsoft i hate your date format", - ?_assert(json_escape(<<"/Date(1303502009425)/">>, - #opts{escape_forward_slash=true} - ) =:= <<"\\/Date(1303502009425)\\/">> + ?_assertEqual( + json_escape(<<"/Date(1303502009425)/">>, #opts{escape_forward_slash=true}), + <<"\\/Date(1303502009425)\\/">> ) } ]. From d2950ab8c73a835de9f618cff7900dbf6d4d75ab Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Fri, 16 Mar 2012 15:34:57 -0700 Subject: [PATCH 32/75] remove all ?_assert and replace with ?_assertFoo's --- src/jsx_encoder.erl | 73 +++++++------ src/jsx_to_json.erl | 258 ++++++++++++++++++++------------------------ src/jsx_to_term.erl | 54 +++++----- src/jsx_verify.erl | 6 +- 4 files changed, 176 insertions(+), 215 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index f4d83e7..0af674c 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -118,49 +118,48 @@ encode(Term) -> (encoder(jsx, [], []))(Term). encode_test_() -> [ - {"naked string", ?_assert(encode(<<"a string">>) - =:= [{string, <<"a string">>}, end_json]) - }, - {"naked integer", ?_assert(encode(123) - =:= [{integer, 123}, end_json]) - }, - {"naked float", ?_assert(encode(1.23) - =:= [{float, 1.23}, end_json]) - }, - {"naked literal", ?_assert(encode(null) - =:= [{literal, null}, end_json]) - }, - {"empty object", ?_assert(encode([{}]) - =:= [start_object, end_object, end_json]) - }, - {"empty list", ?_assert(encode([]) - =:= [start_array, end_array, end_json]) - }, - {"simple list", ?_assert(encode([1,2,3,true,false]) - =:= [start_array, + {"naked string", ?_assertEqual(encode(<<"a string">>), [{string, <<"a string">>}, end_json])}, + {"naked integer", ?_assertEqual(encode(123), [{integer, 123}, end_json])}, + {"naked float", ?_assertEqual(encode(1.23), [{float, 1.23}, end_json])}, + {"naked literal", ?_assertEqual(encode(null), [{literal, null}, end_json])}, + {"empty object", ?_assertEqual(encode([{}]), [start_object, end_object, end_json])}, + {"empty list", ?_assertEqual(encode([]), [start_array, end_array, end_json])}, + {"simple list", ?_assertEqual( + encode([1,2,3,true,false]), + [ + start_array, {integer, 1}, {integer, 2}, {integer, 3}, {literal, true}, {literal, false}, end_array, - end_json]) + end_json + ] + ) }, - {"simple object", ?_assert(encode([{<<"a">>, true}, {<<"b">>, false}]) - =:= [start_object, + {"simple object", ?_assertEqual( + encode([{<<"a">>, true}, {<<"b">>, false}]), + [ + start_object, {key, <<"a">>}, {literal, true}, {key, <<"b">>}, {literal, false}, end_object, - end_json]) + end_json + ] + ) }, - {"complex term", ?_assert(encode([ - {<<"a">>, true}, - {<<"b">>, false}, - {<<"c">>, [1,2,3]}, - {<<"d">>, [{<<"key">>, <<"value">>}]} - ]) =:= [start_object, + {"complex term", ?_assertEqual( + encode([ + {<<"a">>, true}, + {<<"b">>, false}, + {<<"c">>, [1,2,3]}, + {<<"d">>, [{<<"key">>, <<"value">>}]} + ]), + [ + start_object, {key, <<"a">>}, {literal, true}, {key, <<"b">>}, @@ -177,14 +176,14 @@ encode_test_() -> {string, <<"value">>}, end_object, end_object, - end_json]) + end_json + ] + ) }, - {"atom keys", ?_assert(encode([{key, <<"value">>}]) - =:= [start_object, - {key, <<"key">>}, - {string, <<"value">>}, - end_object, - end_json]) + {"atom keys", ?_assertEqual( + encode([{key, <<"value">>}]), + [start_object, {key, <<"key">>}, {string, <<"value">>}, end_object, end_json] + ) } ]. diff --git a/src/jsx_to_json.erl b/src/jsx_to_json.erl index 6d012f5..0e4edf4 100644 --- a/src/jsx_to_json.erl +++ b/src/jsx_to_json.erl @@ -186,176 +186,148 @@ teardown_nicedecimal_meck(_) -> basic_format_test_() -> [ - {"empty object", ?_assert(format(<<"{}">>, []) =:= <<"{}">>)}, - {"empty array", ?_assert(format(<<"[]">>, []) =:= <<"[]">>)}, - {"naked integer", ?_assert(format(<<"123">>, []) =:= <<"123">>)}, + {"empty object", ?_assertEqual(format(<<"{}">>, []), <<"{}">>)}, + {"empty array", ?_assertEqual(format(<<"[]">>, []), <<"[]">>)}, + {"naked integer", ?_assertEqual(format(<<"123">>, []), <<"123">>)}, {foreach, fun() -> setup_nicedecimal_meck(<<"1.23">>) end, fun(R) -> teardown_nicedecimal_meck(R) end, - [{"naked float", ?_assert(format(<<"1.23">>, []) =:= <<"1.23">>)}] - }, - {"naked string", ?_assert(format(<<"\"hi\"">>, []) =:= <<"\"hi\"">>)}, - {"naked literal", ?_assert(format(<<"true">>, []) =:= <<"true">>)}, - {"simple object", - ?_assert(format(<<" { \"key\" :\n\t \"value\"\r\r\r\n } ">>, - [] - ) =:= <<"{\"key\":\"value\"}">> - ) - }, - {"really simple object", - ?_assert(format(<<"{\"k\":\"v\"}">>, []) =:= <<"{\"k\":\"v\"}">>) - }, - {"nested object", - ?_assert(format(<<"{\"k\":{\"k\":\"v\"}, \"j\":{}}">>, [] - ) =:= <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> - ) - }, - {"simple array", - ?_assert(format(<<" [\n\ttrue,\n\tfalse , \n \tnull\n] ">>, - [] - ) =:= <<"[true,false,null]">> - ) - }, - {"really simple array", ?_assert(format(<<"[1]">>, []) =:= <<"[1]">>)}, - {"nested array", ?_assert(format(<<"[[[]]]">>, []) =:= <<"[[[]]]">>)}, - {"nested structures", - ?_assert(format( - <<"[{\"key\":\"value\", - \"another key\": \"another value\", - \"a list\": [true, false] - }, - [[{}]] - ]">>, [] - ) =:= <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> - ) + [{"naked float", ?_assertEqual(format(<<"1.23">>, []), <<"1.23">>)}] }, + {"naked string", ?_assertEqual(format(<<"\"hi\"">>, []), <<"\"hi\"">>)}, + {"naked literal", ?_assertEqual(format(<<"true">>, []), <<"true">>)}, + {"simple object", ?_assertEqual( + format(<<" { \"key\" :\n\t \"value\"\r\r\r\n } ">>, []), + <<"{\"key\":\"value\"}">> + )}, + {"really simple object", ?_assertEqual(format(<<"{\"k\":\"v\"}">>, []) , <<"{\"k\":\"v\"}">>)}, + {"nested object", ?_assertEqual( + format(<<"{\"k\":{\"k\":\"v\"}, \"j\":{}}">>, []), + <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> + )}, + {"simple array", ?_assertEqual( + format(<<" [\n\ttrue,\n\tfalse , \n \tnull\n] ">>, []), + <<"[true,false,null]">> + )}, + {"really simple array", ?_assertEqual(format(<<"[1]">>, []), <<"[1]">>)}, + {"nested array", ?_assertEqual(format(<<"[[[]]]">>, []), <<"[[[]]]">>)}, + {"nested structures", ?_assertEqual( + format(<<"[ + { + \"key\":\"value\", + \"another key\": \"another value\", + \"a list\": [true, false] + }, + [[{}]] + ]">>, []), + <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> + )}, {"simple nested structure", - ?_assert(format(<<"[[],{\"k\":[[],{}],\"j\":{}},[]]">>, [] - ) =:= <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> + ?_assertEqual( + format(<<"[[],{\"k\":[[],{}],\"j\":{}},[]]">>, []), + <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> ) } ]. basic_to_json_test_() -> [ - {"empty object", ?_assert(to_json([{}], []) =:= <<"{}">>)}, - {"empty array", ?_assert(to_json([], []) =:= <<"[]">>)}, - {"naked integer", ?_assert(to_json(123, []) =:= <<"123">>)}, + {"empty object", ?_assertEqual(to_json([{}], []), <<"{}">>)}, + {"empty array", ?_assertEqual(to_json([], []), <<"[]">>)}, + {"naked integer", ?_assertEqual(to_json(123, []), <<"123">>)}, {foreach, fun() -> setup_nicedecimal_meck(<<"1.23">>) end, fun(R) -> teardown_nicedecimal_meck(R) end, - [{"naked float", ?_assert(to_json(1.23, []) =:= <<"1.23">>)}] + [{"naked float", ?_assertEqual(to_json(1.23, []) , <<"1.23">>)}] }, - {"naked string", ?_assert(to_json(<<"hi">>, []) =:= <<"\"hi\"">>)}, - {"naked literal", ?_assert(to_json(true, []) =:= <<"true">>)}, - {"simple object", - ?_assert(to_json( - [{<<"key">>, <<"value">>}], - [] - ) =:= <<"{\"key\":\"value\"}">> - ) - }, - {"nested object", - ?_assert(to_json( - [{<<"k">>,[{<<"k">>,<<"v">>}]},{<<"j">>,[{}]}], - [] - ) =:= <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> - ) - }, - {"simple array", - ?_assert(to_json( - [true, false, null], - [] - ) =:= <<"[true,false,null]">> - ) - }, - {"really simple array", ?_assert(to_json([1], []) =:= <<"[1]">>)}, - {"nested array", ?_assert(to_json([[[]]], []) =:= <<"[[[]]]">>)}, - {"nested structures", - ?_assert(to_json( + {"naked string", ?_assertEqual(to_json(<<"hi">>, []), <<"\"hi\"">>)}, + {"naked literal", ?_assertEqual(to_json(true, []), <<"true">>)}, + {"simple object", ?_assertEqual( + to_json( + [{<<"key">>, <<"value">>}], + [] + ), + <<"{\"key\":\"value\"}">> + )}, + {"nested object", ?_assertEqual( + to_json( + [{<<"k">>,[{<<"k">>,<<"v">>}]},{<<"j">>,[{}]}], + [] + ), + <<"{\"k\":{\"k\":\"v\"},\"j\":{}}">> + )}, + {"simple array", ?_assertEqual(to_json([true, false, null], []), <<"[true,false,null]">>)}, + {"really simple array", ?_assertEqual(to_json([1], []), <<"[1]">>)}, + {"nested array", ?_assertEqual(to_json([[[]]], []), <<"[[[]]]">>)}, + {"nested structures", ?_assertEqual( + to_json( + [ [ - [ - {<<"key">>, <<"value">>}, - {<<"another key">>, <<"another value">>}, - {<<"a list">>, [true, false]} - ], - [[[{}]]] + {<<"key">>, <<"value">>}, + {<<"another key">>, <<"another value">>}, + {<<"a list">>, [true, false]} ], - [] - ) =:= <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> - ) - }, - {"simple nested structure", - ?_assert(to_json( - [[], [{<<"k">>, [[], [{}]]}, {<<"j">>, [{}]}], []], - [] - ) =:= <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> - ) - } + [[[{}]]] + ], + [] + ), + <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> + )}, + {"simple nested structure", ?_assertEqual( + to_json( + [[], [{<<"k">>, [[], [{}]]}, {<<"j">>, [{}]}], []], + [] + ), + <<"[[],{\"k\":[[],{}],\"j\":{}},[]]">> + )} ]. opts_test_() -> [ - {"unspecified indent/space", - ?_assert(format(<<" [\n\ttrue,\n\tfalse,\n\tnull\n] ">>, - [space, indent] - ) =:= <<"[\n true,\n false,\n null\n]">> - ) - }, - {"specific indent/space", - ?_assert(format( - <<"\n{\n\"key\" : [],\n\"another key\" : true\n}\n">>, - [{space, 2}, {indent, 3}] - ) =:= <<"{\n \"key\": [],\n \"another key\": true\n}">> - ) - }, - {"nested structures", - ?_assert(format( - <<"[{\"key\":\"value\", - \"another key\": \"another value\" - }, - [[true, false, null]] - ]">>, - [{space, 2}, {indent, 2}] - ) =:= <<"[\n {\n \"key\": \"value\",\n \"another key\": \"another value\"\n },\n [\n [\n true,\n false,\n null\n ]\n ]\n]">> - ) - }, - {"array spaces", - ?_assert(format(<<"[1,2,3]">>, - [{space, 2}] - ) =:= <<"[1, 2, 3]">> - ) - }, - {"object spaces", - ?_assert(format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, - [{space, 2}] - ) =:= <<"{\"a\": true, \"b\": true, \"c\": true}">> - ) - }, + {"unspecified indent/space", ?_assertEqual( + format(<<" [\n\ttrue,\n\tfalse,\n\tnull\n] ">>, [space, indent]), + <<"[\n true,\n false,\n null\n]">> + )}, + {"specific indent/space", ?_assertEqual( + format( + <<"\n{\n\"key\" : [],\n\"another key\" : true\n}\n">>, + [{space, 2}, {indent, 3}] + ), + <<"{\n \"key\": [],\n \"another key\": true\n}">> + )}, + {"nested structures", ?_assertEqual( + format( + <<"[{\"key\":\"value\", \"another key\": \"another value\"}, [[true, false, null]]]">>, + [{space, 2}, {indent, 2}] + ), + <<"[\n {\n \"key\": \"value\",\n \"another key\": \"another value\"\n },\n [\n [\n true,\n false,\n null\n ]\n ]\n]">> + )}, + {"array spaces", ?_assertEqual( + format(<<"[1,2,3]">>, [{space, 2}]), + <<"[1, 2, 3]">> + )}, + {"object spaces", ?_assertEqual( + format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, [{space, 2}]), + <<"{\"a\": true, \"b\": true, \"c\": true}">> + )}, {foreach, fun() -> setup_nicedecimal_meck(<<"1.23">>) end, fun(R) -> teardown_nicedecimal_meck(R) end, - [{ - "array indent", - ?_assert(format(<<"[1.23, 1.23, 1.23]">>, - [{indent, 2}] - ) =:= <<"[\n 1.23,\n 1.23,\n 1.23\n]">> - ) - }] + [{"array indent", ?_assertEqual( + format(<<"[1.23, 1.23, 1.23]">>, [{indent, 2}]), + <<"[\n 1.23,\n 1.23,\n 1.23\n]">> + )}] }, - {"object indent", - ?_assert(format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, - [{indent, 2}] - ) =:= <<"{\n \"a\":true,\n \"b\":true,\n \"c\":true\n}">> - ) - } + {"object indent", ?_assertEqual( + format(<<"{\"a\":true,\"b\":true,\"c\":true}">>, [{indent, 2}]), + <<"{\n \"a\":true,\n \"b\":true,\n \"c\":true\n}">> + )} ]. ext_opts_test_() -> - [{"extopts", ?_assert(format(<<"[]">>, - [loose_unicode, {escape_forward_slash, true}] - ) =:= <<"[]">> - )} - ]. + [{"extopts", ?_assertEqual( + format(<<"[]">>, [loose_unicode, {escape_forward_slash, true}]), + <<"[]">> + )}]. -endif. \ No newline at end of file diff --git a/src/jsx_to_term.erl b/src/jsx_to_term.erl index 3099829..b5b73bc 100644 --- a/src/jsx_to_term.erl +++ b/src/jsx_to_term.erl @@ -102,35 +102,29 @@ format_key(Key, Opts) -> basic_test_() -> [ - {"empty object", ?_assert(to_term(<<"{}">>, []) =:= [{}])}, - {"simple object", ?_assert(to_term(<<"{\"key\": true}">>, []) =:= [{<<"key">>, true}])}, - {"less simple object", - ?_assert(to_term(<<"{\"a\": 1, \"b\": 2}">>, []) =:= [{<<"a">>, 1}, {<<"b">>, 2}]) - }, - {"nested object", - ?_assert(to_term(<<"{\"key\": {\"key\": true}}">>, []) =:= [{<<"key">>, [{<<"key">>, true}]}]) - }, + {"empty object", ?_assertEqual(to_term(<<"{}">>, []), [{}])}, + {"simple object", ?_assertEqual(to_term(<<"{\"key\": true}">>, []), [{<<"key">>, true}])}, + {"less simple object", ?_assertEqual( + to_term(<<"{\"a\": 1, \"b\": 2}">>, []), + [{<<"a">>, 1}, {<<"b">>, 2}] + )}, + {"nested object", ?_assertEqual( + to_term(<<"{\"key\": {\"key\": true}}">>, []), + [{<<"key">>, [{<<"key">>, true}]}] + )}, {"empty array", ?_assert(to_term(<<"[]">>, []) =:= [])}, - {"list of lists", - ?_assert(to_term(<<"[[],[],[]]">>, []) =:= [[], [], []]) - }, - {"list of strings", - ?_assert(to_term(<<"[\"hi\", \"there\"]">>, []) =:= [<<"hi">>, <<"there">>]) - }, - {"list of numbers", - ?_assert(to_term(<<"[1, 2.0, 3e4, -5]">>, []) =:= [1, 2.0, 3.0e4, -5]) - }, - {"list of literals", - ?_assert(to_term(<<"[true,false,null]">>, []) =:= [true,false,null]) - }, - {"list of objects", - ?_assert(to_term(<<"[{}, {\"a\":1, \"b\":2}, {\"key\":[true,false]}]">>, []) - =:= [[{}], [{<<"a">>,1},{<<"b">>,2}], [{<<"key">>,[true,false]}]]) - } + {"list of lists", ?_assertEqual(to_term(<<"[[],[],[]]">>, []), [[], [], []])}, + {"list of strings", ?_assertEqual(to_term(<<"[\"hi\", \"there\"]">>, []), [<<"hi">>, <<"there">>])}, + {"list of numbers", ?_assertEqual(to_term(<<"[1, 2.0, 3e4, -5]">>, []), [1, 2.0, 3.0e4, -5])}, + {"list of literals", ?_assertEqual(to_term(<<"[true,false,null]">>, []), [true,false,null])}, + {"list of objects", ?_assertEqual( + to_term(<<"[{}, {\"a\":1, \"b\":2}, {\"key\":[true,false]}]">>, []), + [[{}], [{<<"a">>,1},{<<"b">>,2}], [{<<"key">>,[true,false]}]] + )} ]. comprehensive_test_() -> - {"comprehensive test", ?_assert(to_term(comp_json(), []) =:= comp_term())}. + {"comprehensive test", ?_assertEqual(to_term(comp_json(), []), comp_term())}. comp_json() -> <<"[ @@ -157,7 +151,7 @@ comp_term() -> ]. atom_labels_test_() -> - {"atom labels test", ?_assert(to_term(comp_json(), [{labels, atom}]) =:= atom_term())}. + {"atom labels test", ?_assertEqual(to_term(comp_json(), [{labels, atom}]), atom_term())}. atom_term() -> [ @@ -173,10 +167,10 @@ atom_term() -> naked_test_() -> [ - {"naked integer", ?_assert(to_term(<<"123">>, []) =:= 123)}, - {"naked float", ?_assert(to_term(<<"-4.32e-17">>, []) =:= -4.32e-17)}, - {"naked literal", ?_assert(to_term(<<"true">>, []) =:= true)}, - {"naked string", ?_assert(to_term(<<"\"string\"">>, []) =:= <<"string">>)} + {"naked integer", ?_assertEqual(to_term(<<"123">>, []), 123)}, + {"naked float", ?_assertEqual(to_term(<<"-4.32e-17">>, []), -4.32e-17)}, + {"naked literal", ?_assertEqual(to_term(<<"true">>, []), true)}, + {"naked string", ?_assertEqual(to_term(<<"\"string\"">>, []), <<"string">>)} ]. -endif. \ No newline at end of file diff --git a/src/jsx_verify.erl b/src/jsx_verify.erl index d49c90f..1a91b37 100644 --- a/src/jsx_verify.erl +++ b/src/jsx_verify.erl @@ -169,11 +169,7 @@ term_true_test_() -> {"empty array", ?_assert(is_term([], []))}, {"whitespace", ?_assert(is_term([ true ], []))}, {"nested terms", - ?_assert(is_term( - [[{x, [[{}], [{}], [{}]]}, {y, [{}]}], [{}], [[[]]]], - [] - ) - ) + ?_assert(is_term([[{x, [[{}], [{}], [{}]]}, {y, [{}]}], [{}], [[[]]]], [])) }, {"numbers", ?_assert(is_term([-1.0, -1, -0, 0, 1.0e-1, 1, 1.0, 1.0e1], [])) From e3c883457f4de2f28d569baf135ec627367d55cc Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 19 Mar 2012 14:34:07 -0700 Subject: [PATCH 33/75] allow c and c++ style comments anywhere whitespace is legal --- src/jsx_decoder.erl | 267 +++++++++++++++++++++++++++++++++++++++++++- src/jsx_opts.hrl | 3 +- src/jsx_utils.erl | 5 +- 3 files changed, 271 insertions(+), 4 deletions(-) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 006cc22..eb85efa 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -77,6 +77,9 @@ decoder(Handler, State, Opts) -> -define(negative, 16#2D). -define(positive, 16#2B). +%% comments +-define(star, 16#2A). + %% some useful guards -define(is_hex(Symbol), @@ -153,6 +156,9 @@ value(<>, {Handler, State}, Stack, Opts) -> array(Rest, {Handler, Handler:handle_event(start_array, State)}, [array|Stack], Opts); value(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> value(Rest, Handler, Stack, Opts); +value(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> value(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); value(<<>>, Handler, Stack, Opts) -> ?incomplete(value, <<>>, Handler, Stack, Opts); value(Bin, Handler, Stack, Opts) -> @@ -167,6 +173,9 @@ object(<>, {Handler, State}, [key|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); object(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> object(Rest, Handler, Stack, Opts); +object(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> object(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); object(<<>>, Handler, Stack, Opts) -> ?incomplete(object, <<>>, Handler, Stack, Opts); object(Bin, Handler, Stack, Opts) -> @@ -196,7 +205,10 @@ array(<>, {Handler, State}, Stack, Opts) -> array(<>, {Handler, State}, [array|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_array, State)}, Stack, Opts); array(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> - array(Rest, Handler, Stack, Opts); + array(Rest, Handler, Stack, Opts); +array(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> array(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); array(<<>>, Handler, Stack, Opts) -> ?incomplete(array, <<>>, Handler, Stack, Opts); array(Bin, Handler, Stack, Opts) -> @@ -207,6 +219,9 @@ colon(<>, Handler, [key|Stack], Opts) -> value(Rest, Handler, [object|Stack], Opts); colon(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> colon(Rest, Handler, Stack, Opts); +colon(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> colon(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); colon(<<>>, Handler, Stack, Opts) -> ?incomplete(colon, <<>>, Handler, Stack, Opts); colon(Bin, Handler, Stack, Opts) -> @@ -218,7 +233,10 @@ key(<>, Handler, Stack, Opts) -> key(<>, Handler, Stack, Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?new_seq(), single_quote|Stack], Opts); key(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> - key(Rest, Handler, Stack, Opts); + key(Rest, Handler, Stack, Opts); +key(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> key(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); key(<<>>, Handler, Stack, Opts) -> ?incomplete(key, <<>>, Handler, Stack, Opts); key(Bin, Handler, Stack, Opts) -> @@ -478,6 +496,9 @@ zero(<>, Handler, [Acc|Stack], Opts) -> initial_decimal(Rest, Handler, [{Acc, []}|Stack], Opts); zero(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +zero(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); zero(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); zero(<<>>, Handler, Stack, Opts) -> @@ -516,6 +537,9 @@ integer(<>, Handler, [Acc|Stack], Opts) when S =:= $e; S =:= $E e(Rest, Handler, [{Acc, [], []}|Stack], Opts); integer(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +integer(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); integer(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); integer(<<>>, Handler, Stack, Opts) -> @@ -561,6 +585,9 @@ decimal(<>, Handler, [{Int, Frac}|Stack], Opts) e(Rest, Handler, [{Int, Frac, []}|Stack], Opts); decimal(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +decimal(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); decimal(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); decimal(<<>>, Handler, Stack, Opts) -> @@ -615,6 +642,9 @@ exp(<>, {Handler, State}, [Acc, array|Stack], Opts) -> value(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [array|Stack], Opts); exp(<>, {Handler, State}, [Acc|Stack], Opts) when ?is_whitespace(S) -> maybe_done(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); +exp(<>, {Handler, State}, [Acc|Stack], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, {Handler, Handler:handle_event(format_number(Acc), State)}, [Resume|Stack], Opts); exp(<<>>, {Handler, State}, [Acc|Stack], Opts = #opts{explicit_end=false}) -> maybe_done(<<>>, {Handler, Handler:handle_event(format_number(Acc), State)}, Stack, Opts); exp(<<>>, Handler, Stack, Opts) -> @@ -713,6 +743,48 @@ null(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). +comment(<>, Handler, Stack, Opts) -> + single_comment(Rest, Handler, Stack, Opts); +comment(<>, Handler, Stack, Opts) -> + multi_comment(Rest, Handler, Stack, Opts); +comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(comment, <<>>, Handler, Stack, Opts); +comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + +single_comment(<>, Handler, [Resume|Stack], Opts) -> + Resume(Rest, Handler, Stack, Opts); +single_comment(<<>>, Handler, [Resume|Stack], Opts) -> + Resume(<<>>, Handler, Stack, Opts); +single_comment(<<_S/utf8, Rest/binary>>, Handler, Stack, Opts) -> + single_comment(Rest, Handler, Stack, Opts); +single_comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(single_comment, <<>>, Handler, Stack, Opts); +single_comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + +multi_comment(<>, Handler, Stack, Opts) -> + end_multi_comment(Rest, Handler, Stack, Opts); +multi_comment(<<_S/utf8, Rest/binary>>, Handler, Stack, Opts) -> + multi_comment(Rest, Handler, Stack, Opts); +multi_comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(multi_comment, <<>>, Handler, Stack, Opts); +multi_comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + +end_multi_comment(<>, Handler, [Resume|Stack], Opts) -> + Resume(Rest, Handler, Stack, Opts); +end_multi_comment(<<_S/utf8, Rest/binary>>, Handler, Stack, Opts) -> + multi_comment(Rest, Handler, Stack, Opts); +end_multi_comment(<<>>, Handler, Stack, Opts) -> + ?incomplete(end_multi_comment, <<>>, Handler, Stack, Opts); +end_multi_comment(Bin, Handler, Stack, Opts) -> + ?error([Bin, Handler, Stack, Opts]). + + maybe_done(<>, {Handler, State}, [object|Stack], Opts) -> maybe_done(Rest, {Handler, Handler:handle_event(end_object, State)}, Stack, Opts); maybe_done(<>, {Handler, State}, [array|Stack], Opts) -> @@ -723,6 +795,9 @@ maybe_done(<>, Handler, [array|_] = Stack, Opts) -> value(Rest, Handler, Stack, Opts); maybe_done(<>, Handler, Stack, Opts) when ?is_whitespace(S) -> maybe_done(Rest, Handler, Stack, Opts); +maybe_done(<>, Handler, Stack, Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> maybe_done(R, H, S, O) end, + comment(Rest, Handler, [Resume|Stack], Opts); maybe_done(<<>>, Handler, Stack, Opts) when length(Stack) > 0 -> ?incomplete(maybe_done, <<>>, Handler, Stack, Opts); maybe_done(Rest, {Handler, State}, [], Opts) -> @@ -733,6 +808,9 @@ maybe_done(Bin, Handler, Stack, Opts) -> done(<>, Handler, [], Opts) when ?is_whitespace(S) -> done(Rest, Handler, [], Opts); +done(<>, Handler, [], Opts=#opts{comments=true}) -> + Resume = fun(R, H, S, O) -> done(R, H, S, O) end, + comment(Rest, Handler, [Resume], Opts); done(<<>>, {Handler, State}, [], Opts = #opts{explicit_end=true}) -> {incomplete, fun(Stream) when is_binary(Stream) -> done(<>, {Handler, State}, [], Opts) @@ -748,6 +826,191 @@ done(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). -include_lib("eunit/include/eunit.hrl"). +comments_test_() -> + [ + {"preceeding // comment", ?_assertEqual( + decode(<<"// comment ", ?newline, "[]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"preceeding /**/ comment", ?_assertEqual( + decode(<<"/* comment */[]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"trailing // comment", ?_assertEqual( + decode(<<"[]// comment", ?newline>>, [comments]), + [start_array, end_array, end_json] + )}, + {"trailing // comment (no newline)", ?_assertEqual( + decode(<<"[]// comment">>, [comments]), + [start_array, end_array, end_json] + )}, + {"trailing /**/ comment", ?_assertEqual( + decode(<<"[] /* comment */">>, [comments]), + [start_array, end_array, end_json] + )}, + {"// comment inside array", ?_assertEqual( + decode(<<"[ // comment", ?newline, "]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"/**/ comment inside array", ?_assertEqual( + decode(<<"[ /* comment */ ]">>, [comments]), + [start_array, end_array, end_json] + )}, + {"// comment at beginning of array", ?_assertEqual( + decode(<<"[ // comment", ?newline, "true", ?newline, "]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"/**/ comment at beginning of array", ?_assertEqual( + decode(<<"[ /* comment */ true ]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"// comment at end of array", ?_assertEqual( + decode(<<"[ true // comment", ?newline, "]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"/**/ comment at end of array", ?_assertEqual( + decode(<<"[ true /* comment */ ]">>, [comments]), + [start_array, {literal, true}, end_array, end_json] + )}, + {"// comment midarray (post comma)", ?_assertEqual( + decode(<<"[ true, // comment", ?newline, "false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"/**/ comment midarray (post comma)", ?_assertEqual( + decode(<<"[ true, /* comment */ false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"// comment midarray (pre comma)", ?_assertEqual( + decode(<<"[ true// comment", ?newline, ", false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"/**/ comment midarray (pre comma)", ?_assertEqual( + decode(<<"[ true/* comment */, false ]">>, [comments]), + [start_array, {literal, true}, {literal, false}, end_array, end_json] + )}, + {"// comment inside object", ?_assertEqual( + decode(<<"{ // comment", ?newline, "}">>, [comments]), + [start_object, end_object, end_json] + )}, + {"/**/ comment inside object", ?_assertEqual( + decode(<<"{ /* comment */ }">>, [comments]), + [start_object, end_object, end_json] + )}, + {"// comment at beginning of object", ?_assertEqual( + decode(<<"{ // comment", ?newline, " \"key\": true", ?newline, "}">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment at beginning of object", ?_assertEqual( + decode(<<"{ /* comment */ \"key\": true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment at end of object", ?_assertEqual( + decode(<<"{ \"key\": true // comment", ?newline, "}">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment at end of object", ?_assertEqual( + decode(<<"{ \"key\": true /* comment */ }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment midobject (post comma)", ?_assertEqual( + decode(<<"{ \"x\": true, // comment", ?newline, "\"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"/**/ comment midobject (post comma)", ?_assertEqual( + decode(<<"{ \"x\": true, /* comment */", ?newline, "\"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"// comment midobject (pre comma)", ?_assertEqual( + decode(<<"{ \"x\": true// comment", ?newline, ", \"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"/**/ comment midobject (pre comma)", ?_assertEqual( + decode(<<"{ \"x\": true/* comment */", ?newline, ", \"y\": false }">>, [comments]), + [ + start_object, + {key, <<"x">>}, + {literal, true}, + {key, <<"y">>}, + {literal, false}, + end_object, + end_json + ] + )}, + {"// comment precolon", ?_assertEqual( + decode(<<"{ \"key\" // comment", ?newline, ": true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment precolon", ?_assertEqual( + decode(<<"{ \"key\"/* comment */: true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment postcolon", ?_assertEqual( + decode(<<"{ \"key\": // comment", ?newline, " true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"/**/ comment postcolon", ?_assertEqual( + decode(<<"{ \"key\":/* comment */ true }">>, [comments]), + [start_object, {key, <<"key">>}, {literal, true}, end_object, end_json] + )}, + {"// comment terminating zero", ?_assertEqual( + decode(<<"[ 0// comment", ?newline, "]">>, [comments]), + [start_array, {integer, 0}, end_array, end_json] + )}, + {"// comment terminating integer", ?_assertEqual( + decode(<<"[ 1// comment", ?newline, "]">>, [comments]), + [start_array, {integer, 1}, end_array, end_json] + )}, + {"// comment terminating float", ?_assertEqual( + decode(<<"[ 1.0// comment", ?newline, "]">>, [comments]), + [start_array, {float, 1.0}, end_array, end_json] + )}, + {"// comment terminating exp", ?_assertEqual( + decode(<<"[ 1e1// comment", ?newline, "]">>, [comments]), + [start_array, {float, 1.0e1}, end_array, end_json] + )}, + {"/**/ comment terminating zero", ?_assertEqual( + decode(<<"[ 0/* comment */ ]">>, [comments]), + [start_array, {integer, 0}, end_array, end_json] + )}, + {"/**/ comment terminating integer", ?_assertEqual( + decode(<<"[ 1/* comment */ ]">>, [comments]), + [start_array, {integer, 1}, end_array, end_json] + )}, + {"/**/ comment terminating float", ?_assertEqual( + decode(<<"[ 1.0/* comment */ ]">>, [comments]), + [start_array, {float, 1.0}, end_array, end_json] + )}, + {"/**/ comment terminating exp", ?_assertEqual( + decode(<<"[ 1e1/* comment */ ]">>, [comments]), + [start_array, {float, 1.0e1}, end_array, end_json] + )} + ]. + + noncharacters_test_() -> [ {"noncharacters - badjson", diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index 938b8fb..7741585 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -3,5 +3,6 @@ escape_forward_slash = false, explicit_end = false, single_quotes = false, - no_jsonp_escapes = false + no_jsonp_escapes = false, + comments = false }). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 2ab87e9..21f74d2 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -47,6 +47,8 @@ parse_opts([single_quotes|Rest], Opts) -> parse_opts(Rest, Opts#opts{single_quotes=true}); parse_opts([no_jsonp_escapes|Rest], Opts) -> parse_opts(Rest, Opts#opts{no_jsonp_escapes=true}); +parse_opts([comments|Rest], Opts) -> + parse_opts(Rest, Opts#opts{comments=true}); parse_opts(_, _) -> {error, badarg}. @@ -57,7 +59,8 @@ valid_flags() -> escape_forward_slash, explicit_end, single_quotes, - no_jsonp_escapes + no_jsonp_escapes, + comments ]. From e36858d1baf9a0205632de84a098d3698185aa21 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 19 Mar 2012 15:57:00 -0700 Subject: [PATCH 34/75] apply escape_forward_slash option to decoding as well as encoding --- priv/test_cases/string_escapes.json | 2 +- priv/test_cases/string_escapes.test | 1 - src/jsx_decoder.erl | 22 +++++++++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/priv/test_cases/string_escapes.json b/priv/test_cases/string_escapes.json index 461bc67..3c9af78 100644 --- a/priv/test_cases/string_escapes.json +++ b/priv/test_cases/string_escapes.json @@ -1 +1 @@ -["\"", "\\", "\/", "\b", "\f", "\n", "\r", "\t"] \ No newline at end of file +["\"", "\\", "\b", "\f", "\n", "\r", "\t"] \ No newline at end of file diff --git a/priv/test_cases/string_escapes.test b/priv/test_cases/string_escapes.test index 7cd460c..8f6eeed 100644 --- a/priv/test_cases/string_escapes.test +++ b/priv/test_cases/string_escapes.test @@ -2,7 +2,6 @@ {jsx, [start_array, {string,<<"\"">>}, {string,<<"\\">>}, - {string,<<"/">>}, {string,<<"\b">>}, {string,<<"\f">>}, {string,<<"\n">>}, diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index eb85efa..4055b2e 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -348,13 +348,16 @@ escape(<<$r, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, $\r)|Stack], Opts); escape(<<$t, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, $\t)|Stack], Opts); -escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> - escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); -escape(<>, Handler, [Acc|Stack], Opts) - when S =:= ?doublequote; S =:= ?solidus; S =:= ?rsolidus -> - string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, $\\)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts=#opts{escape_forward_slash=true}) -> + string(Rest, Handler, [?acc_seq(Acc, $/)|Stack], Opts); +escape(<>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, $\")|Stack], Opts); escape(<>, Handler, [Acc|Stack], Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?acc_seq(Acc, ?singlequote)|Stack], Opts); +escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> + escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); escape(<<>>, Handler, Stack, Opts) -> ?incomplete(escape, <<>>, Handler, Stack, Opts); escape(Bin, Handler, Stack, Opts) -> @@ -1011,6 +1014,15 @@ comments_test_() -> ]. +escape_forward_slash_test_() -> + [ + {"escape forward slash test", ?_assertEqual( + decode(<<"[ \" \/ \" ]">>, [escape_forward_slash]), + [start_array, {string, <<" / ">>}, end_array, end_json] + )} + ]. + + noncharacters_test_() -> [ {"noncharacters - badjson", From 1870a74d76dd43a97adb7485bc9201b54653cf97 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 19 Mar 2012 16:01:58 -0700 Subject: [PATCH 35/75] apply loose_unicode option to decoder --- src/jsx_utils.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 21f74d2..2c4526e 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -132,6 +132,8 @@ json_escape(<>, Opts, Acc) %% any other legal codepoint json_escape(<>, Opts, Acc) -> json_escape(Rest, Opts, <>); +json_escape(<<_, Rest/binary>>, Opts=#opts{loose_unicode=true}, Acc) -> + json_escape(Rest, Opts, <>); json_escape(<<>>, _Opts, Acc) -> Acc; json_escape(Rest, Opts, Acc) -> @@ -173,6 +175,12 @@ binary_escape_test_() -> <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> ) }, + {"json string loose unicode escaping", + ?_assertEqual( + json_escape(<<16#ffff>>, #opts{loose_unicode=true}), + <<16#fffd/utf8>> + ) + }, {"jsonp protection", ?_assertEqual( json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{}), From 0c04e485a3a6fa713c6169d9560e9c3a3a941828 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 19:36:27 -0700 Subject: [PATCH 36/75] fixes wrongheaded and stupid escaping of strings --- src/jsx_encoder.erl | 85 +++++++++++++++++++++++++++++++++++++++++---- src/jsx_utils.erl | 22 +++--------- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 0af674c..f97bf02 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -25,7 +25,6 @@ -export([encoder/3]). - -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). encoder(Handler, State, Opts) -> @@ -54,7 +53,7 @@ start(Term, {Handler, State}, Opts) -> value(String, {Handler, State}, Opts) when is_binary(String) -> - Handler:handle_event({string, escape(String, {Handler, State}, Opts)}, State); + Handler:handle_event({string, check_string(String, {Handler, State}, Opts)}, State); value(Float, {Handler, State}, _Opts) when is_float(Float) -> Handler:handle_event({float, Float}, State); value(Int, {Handler, State}, _Opts) when is_integer(Int) -> @@ -84,7 +83,7 @@ object([{Key, Value}|Rest], {Handler, State}, Opts) -> Handler, value( Value, - {Handler, Handler:handle_event({key, escape(fix_key(Key), {Handler, State}, Opts)}, State)}, + {Handler, Handler:handle_event({key, check_string(fix_key(Key), {Handler, State}, Opts)}, State)}, Opts ) }, @@ -104,10 +103,70 @@ fix_key(Key) when is_atom(Key) -> fix_key(atom_to_binary(Key, utf8)); fix_key(Key) when is_binary(Key) -> Key. -escape(String, Handler, Opts) -> - try jsx_utils:json_escape(String, Opts) - catch error:badarg -> erlang:error(badarg, [String, Handler, Opts]) - end. +check_string(String, Handler, Opts) -> + case check_string(String) of + true -> String; + false -> + case Opts#opts.loose_unicode of + true -> clean_string(String, <<>>); + false -> erlang:error(badarg, [String, Handler, Opts]) + end + end. + +check_string(<>) when C < 16#fdd0 -> + check_string(Rest); +check_string(<>) when C > 16#fdef, C < 16#fffe -> + check_string(Rest); +check_string(<>) + when C =/= 16#fffe andalso C =/= 16#ffff andalso + C =/= 16#1fffe andalso C =/= 16#1ffff andalso + C =/= 16#2fffe andalso C =/= 16#2ffff andalso + C =/= 16#3fffe andalso C =/= 16#3ffff andalso + C =/= 16#4fffe andalso C =/= 16#4ffff andalso + C =/= 16#5fffe andalso C =/= 16#5ffff andalso + C =/= 16#6fffe andalso C =/= 16#6ffff andalso + C =/= 16#7fffe andalso C =/= 16#7ffff andalso + C =/= 16#8fffe andalso C =/= 16#8ffff andalso + C =/= 16#9fffe andalso C =/= 16#9ffff andalso + C =/= 16#afffe andalso C =/= 16#affff andalso + C =/= 16#bfffe andalso C =/= 16#bffff andalso + C =/= 16#cfffe andalso C =/= 16#cffff andalso + C =/= 16#dfffe andalso C =/= 16#dffff andalso + C =/= 16#efffe andalso C =/= 16#effff andalso + C =/= 16#ffffe andalso C =/= 16#fffff andalso + C =/= 16#10fffe andalso C =/= 16#10ffff -> + check_string(Rest); +check_string(<<>>) -> true; +check_string(<<_, _/binary>>) -> false. + +clean_string(<>, Acc) when C >= 16#fdd0, C =< 16#fdef -> + io:format("1: ~p~n", [C]), + clean_string(Rest, <>); +clean_string(<>, Acc) + when C == 16#fffe orelse C == 16#ffff orelse + C == 16#1fffe orelse C == 16#1ffff orelse + C == 16#2fffe orelse C == 16#2ffff orelse + C == 16#3fffe orelse C == 16#3ffff orelse + C == 16#4fffe orelse C == 16#4ffff orelse + C == 16#5fffe orelse C == 16#5ffff orelse + C == 16#6fffe orelse C == 16#6ffff orelse + C == 16#7fffe orelse C == 16#7ffff orelse + C == 16#8fffe orelse C == 16#8ffff orelse + C == 16#9fffe orelse C == 16#9ffff orelse + C == 16#afffe orelse C == 16#affff orelse + C == 16#bfffe orelse C == 16#bffff orelse + C == 16#cfffe orelse C == 16#cffff orelse + C == 16#dfffe orelse C == 16#dffff orelse + C == 16#efffe orelse C == 16#effff orelse + C == 16#ffffe orelse C == 16#fffff orelse + C == 16#10fffe orelse C == 16#10ffff -> + io:format("2: ~p~n", [C]), + clean_string(Rest, <>); +clean_string(<>, Acc) -> + io:format("3: ~p~n", [C]), + clean_string(Rest, <>); +clean_string(<<>>, Acc) -> Acc. + -ifdef(TEST). @@ -115,6 +174,8 @@ escape(String, Handler, Opts) -> encode(Term) -> (encoder(jsx, [], []))(Term). +encode(Term, Opts) -> (encoder(jsx, [], Opts))(Term). + encode_test_() -> [ @@ -184,6 +245,16 @@ encode_test_() -> encode([{key, <<"value">>}]), [start_object, {key, <<"key">>}, {string, <<"value">>}, end_object, end_json] ) + }, + {"bad string", ?_assertError( + badarg, + encode([<<"a bad string: ", 16#ffff/utf8>>]) + ) + }, + {"allow bad string", ?_assertEqual( + encode([<<"a bad string: ", 16#1ffff/utf8>>], [loose_unicode]), + [start_array, {string, <<"a bad string: ", 16#fffd/utf8>>}, end_array, end_json] + ) } ]. diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 2c4526e..0ca6e8c 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -110,10 +110,7 @@ json_escape(<<$\t, Rest/binary>>, Opts, Acc) -> json_escape(Rest, Opts, <>); %% other control characters json_escape(<>, Opts, Acc) when C >= 0, C < $\s -> - json_escape(Rest, - Opts, - <> - ); + json_escape(Rest, Opts, <>); %% escape forward slashes -- optionally -- to faciliate microsoft's retarded %% date format json_escape(<<$/, Rest/binary>>, Opts=#opts{escape_forward_slash=true}, Acc) -> @@ -125,19 +122,14 @@ json_escape(<>, Opts=#opts{no_jsonp_escapes=true}, Acc) %% escape u+2028 and u+2029 to avoid problems with jsonp json_escape(<>, Opts, Acc) when C == 16#2028; C == 16#2029 -> - json_escape(Rest, - Opts, - <> - ); + json_escape(Rest, Opts, <>); %% any other legal codepoint json_escape(<>, Opts, Acc) -> json_escape(Rest, Opts, <>); -json_escape(<<_, Rest/binary>>, Opts=#opts{loose_unicode=true}, Acc) -> - json_escape(Rest, Opts, <>); json_escape(<<>>, _Opts, Acc) -> Acc; -json_escape(Rest, Opts, Acc) -> - erlang:error(badarg, [Rest, Opts, Acc]). +json_escape(Bin, Opts, Acc) -> + erlang:error(badarg, [Bin, Opts, Acc]). %% convert a codepoint to it's \uXXXX equiv. @@ -175,12 +167,6 @@ binary_escape_test_() -> <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> ) }, - {"json string loose unicode escaping", - ?_assertEqual( - json_escape(<<16#ffff>>, #opts{loose_unicode=true}), - <<16#fffd/utf8>> - ) - }, {"jsonp protection", ?_assertEqual( json_escape(<<226, 128, 168, 226, 128, 169>>, #opts{}), From a89b1d3531620cd342529cfb3673d9ffd533916b Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 19:37:02 -0700 Subject: [PATCH 37/75] largely rewritten readme, hopefully more useful --- README.markdown | 314 +++++++++++++++++++++++++++++++----------------- 1 file changed, 201 insertions(+), 113 deletions(-) diff --git a/README.markdown b/README.markdown index 0464f00..b8da7de 100644 --- a/README.markdown +++ b/README.markdown @@ -1,209 +1,297 @@ -## jsx (v1.0.1) ## +# jsx (v1.0) # -a sane json implementation for erlang, inspired by [yajl][yajl] +a sane [json][json] implementation for erlang, inspired by [yajl][yajl] copyright 2011, 2012 alisdair sullivan jsx is released under the terms of the [MIT][MIT] license -jsx uses [rebar][rebar] and [meck][meck] +jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suite -[![Build Status](https://secure.travis-ci.org/talentdeficit/jsx.png?branch=master)](http://travis-ci.org/talentdeficit/jsx) +[![Build Status](https://secure.travis-ci.org/talentdeficit/jsx.png?branch=develop)](http://travis-ci.org/talentdeficit/jsx) -## api ## +## index ## + +* #### [introduction](#intro) #### +* #### [quickstart](#quickstart) #### +* #### [the api](#api) #### + - [json <-> erlang mapping](#mapping) + - [options](#options) + - [incomplete input](#incompletes) + - [the encoder and decoder](#core) + - [handler callbacks](#handler) + - [converting json to erlang and vice versa](#convert) + - [formatting and minifying json text](#format) + - [verifying json and terms are valid input](#verify) + * #### [acknowledgments](#thanks) #### -**converting json to erlang terms** -parses a JSON text (a utf8 encoded binary) and produces an erlang term (see json <-> erlang mapping details below) +## quickstart ## + +to build the library: `rebar compile` + +to convert a utf8 binary containing a json string into an erlang term: `jsx:to_term(JSON)` + +to convert an erlang term into a utf8 binary containing a json string: `jsx:to_json(Term)` + +to check if a binary is valid json: `jsx:is_json(JSON)` + +to check if a term is valid json: `jsx:is_term(Term)` + +to minify a json string: `jsx:format(JSON)` + + +## api ## + + +### json <-> erlang mapping ### + +**json** | **erlang** +--------------------------------|-------------------------------- +`number` | `integer()` and `float()` +`string` | `binary()` +`true`, `false` and `null` | `true`, `false` and `null` +`array` | `[]` and `[JSON]` +`object` | `[{}]` and `[{binary() OR atom(), JSON}]` + +#### json #### + +json must be a binary encoded in `utf8`. if it's invalid `utf8` or invalid json, it probably won't parse without errors. there are a few non-standard extensions to the parser available that may change that, they are detailed in the options section below + +jsx also supports json fragments; valid json values that are not complete json. that means jsx will parse things like `<<"1">`, `<<"true">>` and `<<"\"hello world\"">>` without problems + +#### erlang #### + +only the erlang terms in the table above are supported. non supported terms result in badarg errors. jsx is never going to support erlang lists instead of binaries, mostly because you can't discriminate between lists of integers and strings without hinting, and hinting is silly + +#### numbers #### + +javascript and thus json represent all numeric values with floats. as this is woefully insufficient for many uses, **jsx**, just like erlang, supports bigints. whenever possible, this library will interpret json numbers that look like integers as integers. other numbers will be converted to erlang's floating point type, which is nearly but not quite iee754. negative zero is not representable in erlang (zero is unsigned in erlang and `0` is equivalent to `-0`) and will be interpreted as regular zero. numbers not representable are beyond the concern of this implementation, and will result in parsing errors + +when converting from erlang to json, numbers are represented with their shortest representation that will round trip without loss of precision. this means that some floats may be superficially dissimilar (although functionally equivalent). for example, `1.0000000000000001` will be represented by `1.0` + +#### strings #### + +the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters and quotes) and that are encoded in `utf8`. in the interests of pragmatism, there is an option for looser parsing, see options below + +all erlang strings are represented by *valid* `utf8` encoded binaries + +this implementation performs no normalization on strings beyond that detailed here. be careful when comparing strings as equivalent strings may have different `utf8` encodings + +#### true, false and null #### + +the json primitives `true`, `false` and `null` are represented by the erlang atoms `true`, `false` and `null`. surprise + +#### arrays #### + +json arrays are represented with erlang lists of json values as described in this section + +#### objects #### + +json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties. all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers) + + +### options ### + +jsx functions all take a common set of options. not all flags have meaning in all contexts, but they are always valid options. flags are always atoms and have no value. functions may have additional options beyond these, see individual function documentation for details + +#### `loose_unicode` #### + +json text input and json strings SHOULD be utf8 encoded binaries, appropriately escaped as per the json spec. if this option is present attempts are made to replace invalid codepoints with `u+FFFD` as per the unicode spec + +#### `escape_forward_slash` #### + +json strings are escaped according to the json spec. this means forward slashes are never escaped. unfortunately, a microsoft implementation of json uses escaped forward slashes in json formatted date strings. without this option it is impossible to get date strings that some microsoft tools understand + +#### `explicit_end` #### + +this option treats all exhausted inputs as incomplete, as explained below. the parser will not attempt to return a final state until the function is called with the value `end_stream` + +#### `single_quotes` #### + +some parsers allow double quotes (`u+0022`) to be replaced by single quotes (`u+0027`) to deliminate keys and strings. this option allows json containing single quotes as structural (deliminator) characters to be parsed without errors. note that the parser expects strings to be terminated by the same quote type that opened it and that single quotes must, obviously, be escaped within strings deliminated by single quotes. the parser will never emit json with keys or strings deliminated by single quotes + +#### `no_jsonp_escapes` #### + +javascript interpreters treat the codepoints `u+2028` and `u+2029` as significant whitespace. json strings that contain either of these codepoints will be parsed incorrectly by some javascript interpreters. by default, these codepoints are escaped (to `"\u2028"` and `\u2029`, respectively) to retain compatibility. this option simply removes that escaping if, for some reason, you object to this + +#### `comments` #### + +json has no official comments but some parsers allow c style comments. this flag allows comments (both `// ...` and `/* ... */` style) anywhere whitespace is allowed + + +### incomplete input ### + +jsx handles incomplete json texts. if a partial json text is parsed, rather than returning a term from your callback handler, jsx returns `{incomplete, F}` where `F` is a function with an identical API to the anonymous fun returned from `decoder/3`. it retains the internal state of the parser at the point where input was exhausted. this allows you to parse as you stream json over a socket or file descriptor or to parse large json texts without needing to keep them entirely in memory + +however, it is important to recognize that jsx is greedy by default. if input is exhausted and the json text is not unambiguously incomplete jsx will consider the parsing complete. this is mostly relevant when parsing bare numbers like `<<"1234">>`. this could be a complete json integer or just the beginning of a json integer that is being parsed incrementally. jsx will treat it as a whole integer. the option `explicit_end` can be used to modify this behaviour, see above + + +### the encoder and decoder ### + +jsx is built on top of two finite state automata, one that handles json texts and one that handles erlang terms. both take a callback module as an argument that acts similar to a fold over a list of json 'events'. these events and the handler module's callbacks are detailed in the next section + +`jsx:decoder/3` and `jsx:encoder/3` are the entry points for the decoder and encoder, respectively + +`decoder(Handler, InitialState, Opts)` -> `Fun((JSON) -> Any)` +`encoder(Handler, InitialState, Opts)` -> `Fun((Term) -> Any)` + +types: + +- `Handler` = `atom()`, should be the name of a callback module, see below +- `InitialState` = `term()`, passed as is to `Handler:init/1` +- `Opts` = see above +- `JSON` = `utf8` encoded json text +- `Term` = an erlang term as specified above in the mapping section +- `Any` = `term()` + +decoder returns an anonymous function that handles binary json input and encoder returns an anonymous function that handles erlang term input. these are safe to reuse for multiple inputs + + +### handler callbacks ### + +`Handler` should export the following pair of functions + +`Handler:init(InitialState)` -> `State` +`Handler:handle_event(Event, State)` -> `NewState` + +types: + +- `InitialState`, `State`, `NewState` = any erlang term +- `Event` = + * `start_object` + * `end_object` + * `start_array` + * `end_array` + * `end_json` + * `{key, binary()}` + * `{string, binary()}` + * `{integer, integer()}` + * `{float, float()}` + * `{literal, true}` + * `{literal, false}` + * `{literal, null}` + +`init/1` is called with the `initialState` argument from `decoder/3` or `encoder/3` and should take care of any initialization your handler requires and return a new state + +`handle_event/2` is called for each `Event` emitted by the decoder/encoder with the output of the previous `handle_event/2` call (or `init/1` call, if `handle_event/2` has not yet been called) + +the event `end_json` will always be the last event emitted, you should take care of any cleanup in `handle_event/2` when encountering `end_json`. the state returned from this call will be returned as the final result of the anonymous function + +both `key` and `string` are `utf8` encoded binaries with all escaped values converted into the appropriate codepoints + + +### converting json to erlang and vice versa ### + +#### converting json to erlang terms #### + +`to_term` parses a JSON text (a utf8 encoded binary) and produces an erlang term (see json <-> erlang mapping details above) `to_term(JSON)` -> `Term` - `to_term(JSON, Opts)` -> `Term` types: -* `JSON` = `binary()` -* `Term` = `[]` | `[{}]` | `[Value]` | `[{Label, Value}]` | `{incomplete, Fun}` -* `Value` = `binary()` | `integer()` | `float()` | `true` | `false` | `null` -* `Label` = `binary()` | `atom()` -* `Fun` = `fun(JSON)` -> `Term` -* `Opts` = `[]` | `[Opt]` +* `JSON` = as above in the mapping section +* `Term` = as above in the mapping section +* `Opts` = as above in the opts section, but see also additional opts below * `Opt` = - - `loose_unicode` - - `single_quotes` - `labels` - `{labels, Label}` - `Label` = * `binary` * `atom` * `existing_atom` - - `explicit_end` - -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors - -valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `labels` controls how keys are converted from json to erlang terms. `binary` does no conversion beyond normal escaping. `atom` converts keys to erlang atoms, and results in a badarg error if keys fall outside the range of erlang atoms. `existing_atom` is identical to `atom`, except it will not add new atoms to the atom table -see the note below about streaming mode for details of `explicit_end` - -**converting erlang terms to json** +#### converting erlang terms to json #### -produces a JSON text from an erlang term (see json <-> erlang mapping details below) +`to_json` parses an erlang term and produces a JSON text (see json <-> erlang mapping details below) `to_json(Term)` -> `JSON` - `to_json(Term, Opts)` -> `JSON` types: -* `JSON` = `binary()` -* `Term` = `[]` | `[{}]` | `[Value]` | `[{Label, Value}]` | `{incomplete, Fun}` -* `Value` = `binary()` | `integer()` | `float()` | `true` | `false` | `null` -* `Label` = `binary()` | `atom()` -* `Opts` = `[]` | `[Opt]` +* `JSON` = as above in the mapping section +* `Term` = as above in the mapping section +* `Opts` = as above in the opts section, but see also additional opts below * `Opt` = - `space` - `{space, N}` - `indent` - `{indent, N}` - - `escape_forward_slash` the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` the option `{indent, N}` inserts a newline and `N` spaces for each level of indentation in your json output. note that this overrides spaces inserted after a comma. `indent` is an alias for `{indent, 1}`. the default is `{indent, 0}` - -if the option `escape_forward_slash` is enabled, `$/` is escaped. this is not normally required but is necessary for compatibility with microsoft's json date format -**formatting json texts** +### formatting and minifying json text ### + +#### formatting json texts #### produces a JSON text from JSON text, reformatted `format(JSON)` -> `JSON` - `format(JSON, Opts)` -> `JSON` types: -* `JSON` = `binary()` -* `Term` = `[]` | `[{}]` | `[Value]` | `[{Label, Value}]` | `{incomplete, Fun}` -* `Value` = `binary()` | `integer()` | `float()` | `true` | `false` | `null` -* `Label` = `binary()` | `atom()` -* `Fun` = `fun(JSON)` -> `Term` -* `Opts` = `[]` | `[Opt]` +* `JSON` = as above in the mapping section +* `Opts` = as above in the opts section, but see also additional opts below * `Opt` = - `space` - `{space, N}` - `indent` - `{indent, N}` - - `loose_unicode` - - `single_quotes` - - `escape_forward_slash` - - `explicit_end` - -`JSON` SHOULD be a utf8 encoded binary. if the option `loose_unicode` is present attempts are made to replace invalid codepoints with `u+FFFD` but badly encoded binaries may, in either case, result in `badarg` errors - -valid json strings are deliminated by double quotes, but some implementations allow single quotes in their place. the `single_quotes` option recognizes json texts with single quotes in the place of double quotes as valid. please be aware that if you enable this option, you MUST escape single quotes in keys and strings the option `{space, N}` inserts `N` spaces after every comma and colon in your json output. `space` is an alias for `{space, 1}`. the default is `{space, 0}` the option `{indent, N}` inserts a newline and `N` spaces for each level of indentation in your json output. note that this overrides spaces inserted after a comma. `indent` is an alias for `{indent, 1}`. the default is `{indent, 0}` -if the option `escape_forward_slash` is enabled, `$/` is escaped. this is not normally required but is necessary for compatibility with microsoft's json date format - -see the note below about streaming mode for details of `explicit_end` +calling `format` with no options results in minified json text -**verifying json texts** +### verifying json and terms are valid input ### + +#### verifying json texts #### returns true if input is a valid JSON text, false if not `is_json(MaybeJSON)` -> `true` | `false` | `{incomplete, Fun}` - `is_json(MaybeJSON, Opts)` -> `true` | `false` | `{incomplete, Fun}` types: * `MaybeJSON` = `any()` -* `Opts` = `[]` | `[Opt]` -* `Opt` = - - `loose_unicode` - - `single_quotes` - - `explicit_end` - -see `json_to_term` for details of options +* `Opts` = as above -**verifying json texts** +#### verifying terms #### returns true if input is a valid erlang term that represents a JSON text, false if not `is_term(MaybeJSON)` -> `true` | `false` +`is_term(MaybeJSON, Opts)` -> `true` | `false` types: * `MaybeJSON` = `any()` +* `Opts` = as above -**streaming mode** - -this implementation is interruptable and reentrant and may be used to incrementally parse json texts. it's greedy and will exhaust input, returning when the stream buffer is empty. if the json text is so far valid, but incomplete (or if the option `explicit_end` has been selected), `{incomplete, Fun}` will be returned. `Fun/1` may be called with additional input (or the atom `end_stream` to force the end of parsing) - -`explicit_end` is of use when parsing bare numbers (like `123` or `-0.987` for example) as they may have no unambiguous end when encountered in a stream. it is also of use when reading from a socket or file and there may be unprocessed white space (or errors) left in the stream - - -## json <-> erlang ## - -**json** | **erlang** ---------------------------------|-------------------------------- -`number` | `integer()` OR `float()` -`string` | `binary()` -`true`, `false` and `null` | `true`, `false` and `null` -`array` | `[]` OR `[JSON]` -`object` | `[{}]` OR `[{binary(), JSON}]` - -**json** - -json must be encoded in `utf8`. if it's invalid `utf8`, it probably won't parse without errors. one optional exception is made for json strings that are otherwise `utf8`, see under `strings` below. - -**numbers** - -javascript and thus json represent all numeric values with floats. as this is woefully insufficient for many uses, **jsx**, just like erlang, supports bigints. whenever possible, this library will interpret json numbers that look like integers as integers. other numbers will be converted to erlang's floating point type, which is nearly but not quite iee754. negative zero is not representable in erlang (zero is unsigned in erlang and `0` is equivalent to `-0`) and will be interpreted as regular zero. numbers not representable are beyond the concern of this implementation, and will result in parsing errors - -when converting from erlang to json, numbers are represented with their shortest representation that will round trip without loss of precision. this means that some floats may be superficially dissimilar (although functionally equivalent). for example, `1.0000000000000001` will be represented by `1.0` - -**strings** - -the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters and quotes) and that are encoded in `utf8`. in the interests of pragmatism, however, the parser option `loose_unicode` attempts to replace invalid `utf8` sequences with the replacement codepoint `u+fffd` when possible - -all erlang strings are represented by *valid* `utf8` encoded binaries - -this implementation performs no normalization on strings beyond that detailed here. be careful when comparing strings as equivalent strings may have different `utf8` encodings - -**true, false and null** - -the json primitives `true`, `false` and `null` are represented by the erlang atoms `true`, `false` and `null`. surprise - -**arrays** - -json arrays are represented with erlang lists of json values as described in this document - -**objects** - -json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties. all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers) - - -## acknowledgements ## +## acknowledgements ## paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides and alex kropivny have all contributed to the development of jsx, whether they know it or not +[json]: http://json.org [yajl]: http://lloyd.github.com/yajl [MIT]: http://www.opensource.org/licenses/mit-license.html [rebar]: https://github.com/basho/rebar [meck]: https://github.com/eproxus/meck -[json]: http://json.org [rfc4627]: http://tools.ietf.org/html/rfc4627 \ No newline at end of file From 83cd03f6a31da3f7a898373185adb81280aab83e Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 19:42:03 -0700 Subject: [PATCH 38/75] readme fixes for github's markdown --- README.markdown | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index b8da7de..89aa2d8 100644 --- a/README.markdown +++ b/README.markdown @@ -13,9 +13,9 @@ jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suit ## index ## -* #### [introduction](#intro) #### -* #### [quickstart](#quickstart) #### -* #### [the api](#api) #### +* [introduction](#intro) +* [quickstart](#quickstart) +* [the api](#api) - [json <-> erlang mapping](#mapping) - [options](#options) - [incomplete input](#incompletes) @@ -24,7 +24,7 @@ jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suit - [converting json to erlang and vice versa](#convert) - [formatting and minifying json text](#format) - [verifying json and terms are valid input](#verify) - * #### [acknowledgments](#thanks) #### + * [acknowledgments](#thanks) @@ -136,6 +136,7 @@ jsx is built on top of two finite state automata, one that handles json texts an `jsx:decoder/3` and `jsx:encoder/3` are the entry points for the decoder and encoder, respectively `decoder(Handler, InitialState, Opts)` -> `Fun((JSON) -> Any)` + `encoder(Handler, InitialState, Opts)` -> `Fun((Term) -> Any)` types: @@ -155,6 +156,7 @@ decoder returns an anonymous function that handles binary json input and encoder `Handler` should export the following pair of functions `Handler:init(InitialState)` -> `State` + `Handler:handle_event(Event, State)` -> `NewState` types: @@ -190,6 +192,7 @@ both `key` and `string` are `utf8` encoded binaries with all escaped values conv `to_term` parses a JSON text (a utf8 encoded binary) and produces an erlang term (see json <-> erlang mapping details above) `to_term(JSON)` -> `Term` + `to_term(JSON, Opts)` -> `Term` types: @@ -212,6 +215,7 @@ the option `labels` controls how keys are converted from json to erlang terms. ` `to_json` parses an erlang term and produces a JSON text (see json <-> erlang mapping details below) `to_json(Term)` -> `JSON` + `to_json(Term, Opts)` -> `JSON` types: @@ -237,6 +241,7 @@ the option `{indent, N}` inserts a newline and `N` spaces for each level of inde produces a JSON text from JSON text, reformatted `format(JSON)` -> `JSON` + `format(JSON, Opts)` -> `JSON` types: @@ -263,6 +268,7 @@ calling `format` with no options results in minified json text returns true if input is a valid JSON text, false if not `is_json(MaybeJSON)` -> `true` | `false` | `{incomplete, Fun}` + `is_json(MaybeJSON, Opts)` -> `true` | `false` | `{incomplete, Fun}` types: @@ -276,6 +282,7 @@ types: returns true if input is a valid erlang term that represents a JSON text, false if not `is_term(MaybeJSON)` -> `true` | `false` + `is_term(MaybeJSON, Opts)` -> `true` | `false` types: From 036dd72ecf9d3c886cbf618a8250dddcdf19a22d Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 22:42:58 -0700 Subject: [PATCH 39/75] minor fixes for illegal utf8 sequences and better testing thereof --- src/jsx_encoder.erl | 130 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 14 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index f97bf02..9e362ee 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -118,7 +118,8 @@ check_string(<>) when C < 16#fdd0 -> check_string(<>) when C > 16#fdef, C < 16#fffe -> check_string(Rest); check_string(<>) - when C =/= 16#fffe andalso C =/= 16#ffff andalso + when C > 16#fffd andalso + C =/= 16#fffe andalso C =/= 16#ffff andalso C =/= 16#1fffe andalso C =/= 16#1ffff andalso C =/= 16#2fffe andalso C =/= 16#2ffff andalso C =/= 16#3fffe andalso C =/= 16#3ffff andalso @@ -140,7 +141,6 @@ check_string(<<>>) -> true; check_string(<<_, _/binary>>) -> false. clean_string(<>, Acc) when C >= 16#fdd0, C =< 16#fdef -> - io:format("1: ~p~n", [C]), clean_string(Rest, <>); clean_string(<>, Acc) when C == 16#fffe orelse C == 16#ffff orelse @@ -160,11 +160,13 @@ clean_string(<>, Acc) C == 16#efffe orelse C == 16#effff orelse C == 16#ffffe orelse C == 16#fffff orelse C == 16#10fffe orelse C == 16#10ffff -> - io:format("2: ~p~n", [C]), clean_string(Rest, <>); clean_string(<>, Acc) -> - io:format("3: ~p~n", [C]), clean_string(Rest, <>); +clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> + clean_string(Rest, <>); +clean_string(<<_, Rest/binary>>, Acc) -> + clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. @@ -174,7 +176,10 @@ clean_string(<<>>, Acc) -> Acc. encode(Term) -> (encoder(jsx, [], []))(Term). -encode(Term, Opts) -> (encoder(jsx, [], Opts))(Term). +encode(Term, Opts) -> + try (encoder(jsx, [], Opts))(Term) + catch _:_ -> {error, badjson} + end. encode_test_() -> @@ -245,17 +250,114 @@ encode_test_() -> encode([{key, <<"value">>}]), [start_object, {key, <<"key">>}, {string, <<"value">>}, end_object, end_json] ) + } + ]. + +noncharacters_test_() -> + [ + {"noncharacters - badjson", + ?_assertEqual(check_bad(noncharacters()), []) }, - {"bad string", ?_assertError( - badarg, - encode([<<"a bad string: ", 16#ffff/utf8>>]) - ) - }, - {"allow bad string", ?_assertEqual( - encode([<<"a bad string: ", 16#1ffff/utf8>>], [loose_unicode]), - [start_array, {string, <<"a bad string: ", 16#fffd/utf8>>}, end_array, end_json] - ) + {"noncharacters - replaced", + ?_assertEqual(check_replaced(noncharacters()), []) } ]. +extended_noncharacters_test_() -> + [ + {"extended noncharacters - badjson", + ?_assertEqual(check_bad(extended_noncharacters()), []) + }, + {"extended noncharacters - replaced", + ?_assertEqual(check_replaced(extended_noncharacters()), []) + } + ]. + +surrogates_test_() -> + [ + {"surrogates - badjson", + ?_assertEqual(check_bad(surrogates()), []) + }, + {"surrogates - replaced", + ?_assertEqual(check_replaced(surrogates()), []) + } + ]. + +reserved_test_() -> + [ + {"reserved noncharacters - badjson", + ?_assertEqual(check_bad(reserved_space()), []) + }, + {"reserved noncharacters - replaced", + ?_assertEqual(check_replaced(reserved_space()), []) + } + ]. + +good_characters_test_() -> + [ + {"acceptable codepoints", + ?_assertEqual(check_good(good()), []) + }, + {"acceptable extended", + ?_assertEqual(check_good(good_extended()), []) + } + ]. + + +check_bad(List) -> + lists:dropwhile(fun({_, {error, badjson}}) -> true ; (_) -> false end, + check(List, [], []) + ). + +check_replaced(List) -> + lists:dropwhile(fun({_, [{string, <<16#fffd/utf8>>}|_]}) -> true + ; (_) -> false + end, + check(List, [loose_unicode], []) + ). + +check_good(List) -> + lists:dropwhile(fun({_, [{string, _}|_]}) -> true ; (_) -> false end, + check(List, [], []) + ). + +check([], _Opts, Acc) -> Acc; +check([H|T], Opts, Acc) -> + R = encode(to_fake_utf(H, utf8), Opts), + check(T, Opts, [{H, R}] ++ Acc). + + + +noncharacters() -> lists:seq(16#fffe, 16#ffff). + +extended_noncharacters() -> + [16#1fffe, 16#1ffff, 16#2fffe, 16#2ffff] + ++ [16#3fffe, 16#3ffff, 16#4fffe, 16#4ffff] + ++ [16#5fffe, 16#5ffff, 16#6fffe, 16#6ffff] + ++ [16#7fffe, 16#7ffff, 16#8fffe, 16#8ffff] + ++ [16#9fffe, 16#9ffff, 16#afffe, 16#affff] + ++ [16#bfffe, 16#bffff, 16#cfffe, 16#cffff] + ++ [16#dfffe, 16#dffff, 16#efffe, 16#effff] + ++ [16#ffffe, 16#fffff, 16#10fffe, 16#10ffff]. + +surrogates() -> lists:seq(16#d800, 16#dfff). + +reserved_space() -> lists:seq(16#fdd0, 16#fdef). + +good() -> lists:seq(1, 16#d7ff) ++ lists:seq(16#e000, 16#fdcf) ++ lists:seq(16#fdf0, 16#fffd). + +good_extended() -> lists:seq(16#100000, 16#10fffd). + +%% erlang refuses to encode certain codepoints, so fake them all +to_fake_utf(N, utf8) when N < 16#0080 -> <>; +to_fake_utf(N, utf8) when N < 16#0800 -> + <<0:5, Y:5, X:6>> = <>, + <<2#110:3, Y:5, 2#10:2, X:6>>; +to_fake_utf(N, utf8) when N < 16#10000 -> + <> = <>, + <<2#1110:4, Z:4, 2#10:2, Y:6, 2#10:2, X:6>>; +to_fake_utf(N, utf8) -> + <<0:3, W:3, Z:6, Y:6, X:6>> = <>, + <<2#11110:5, W:3, 2#10:2, Z:6, 2#10:2, Y:6, 2#10:2, X:6>>. + -endif. \ No newline at end of file From 11d2d0bae1d10b8272635a913efba88393f27132 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 22:47:16 -0700 Subject: [PATCH 40/75] minor fixes for illegal utf8 sequences and better testing thereof --- src/jsx_encoder.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 9e362ee..a2c5bbc 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -161,10 +161,10 @@ clean_string(<>, Acc) C == 16#ffffe orelse C == 16#fffff orelse C == 16#10fffe orelse C == 16#10ffff -> clean_string(Rest, <>); -clean_string(<>, Acc) -> - clean_string(Rest, <>); clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); +clean_string(<>, Acc) -> + clean_string(Rest, <>); clean_string(<<_, Rest/binary>>, Acc) -> clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. From 7b2c34ccd241a47b4b8a55cf2577377787bd8c3d Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 22:53:18 -0700 Subject: [PATCH 41/75] fix for older erts versions where the private space reserved characters are not recognized --- src/jsx_encoder.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index a2c5bbc..9c10a19 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -163,6 +163,8 @@ clean_string(<>, Acc) clean_string(Rest, <>); clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); +clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> + clean_string(Rest, <>); clean_string(<>, Acc) -> clean_string(Rest, <>); clean_string(<<_, Rest/binary>>, Acc) -> From f991f7c42e30bf43e6898fc81cbf1ce31c031f92 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:00:33 -0700 Subject: [PATCH 42/75] fix for older erts that don't allow noncharacters --- src/jsx_encoder.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 9c10a19..21e5269 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -165,6 +165,8 @@ clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> clean_string(Rest, <>); +clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190, X == 191 -> + clean_string(Rest, <>); clean_string(<>, Acc) -> clean_string(Rest, <>); clean_string(<<_, Rest/binary>>, Acc) -> From ad8f640aac61171a859414166c2c09c76d6bf014 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:07:13 -0700 Subject: [PATCH 43/75] remove rogue function head --- src/jsx_encoder.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 21e5269..6f993f1 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -140,8 +140,6 @@ check_string(<>) check_string(<<>>) -> true; check_string(<<_, _/binary>>) -> false. -clean_string(<>, Acc) when C >= 16#fdd0, C =< 16#fdef -> - clean_string(Rest, <>); clean_string(<>, Acc) when C == 16#fffe orelse C == 16#ffff orelse C == 16#1fffe orelse C == 16#1ffff orelse @@ -161,10 +159,13 @@ clean_string(<>, Acc) C == 16#ffffe orelse C == 16#fffff orelse C == 16#10fffe orelse C == 16#10ffff -> clean_string(Rest, <>); +%% surrogates clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); +%% private use noncharacters clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> clean_string(Rest, <>); +%% u+fffe and u+ffff (required for otp < r15) clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190, X == 191 -> clean_string(Rest, <>); clean_string(<>, Acc) -> From 7d99e64d31258ca555ad3db530db6a89c27fcba0 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:13:27 -0700 Subject: [PATCH 44/75] finally found actual cause of otp r14x bug --- src/jsx_encoder.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 6f993f1..35c0af2 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -118,7 +118,7 @@ check_string(<>) when C < 16#fdd0 -> check_string(<>) when C > 16#fdef, C < 16#fffe -> check_string(Rest); check_string(<>) - when C > 16#fffd andalso + when C > 16#ffff andalso C =/= 16#fffe andalso C =/= 16#ffff andalso C =/= 16#1fffe andalso C =/= 16#1ffff andalso C =/= 16#2fffe andalso C =/= 16#2ffff andalso From 3a2b48db1854c123c7b03e81ddbd9a94d2c096dc Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:17:57 -0700 Subject: [PATCH 45/75] ok, now it's fixed for older releases --- src/jsx_encoder.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 35c0af2..f6c0c2f 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -24,6 +24,7 @@ -module(jsx_encoder). -export([encoder/3]). +-compile(export_all). -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). @@ -118,8 +119,7 @@ check_string(<>) when C < 16#fdd0 -> check_string(<>) when C > 16#fdef, C < 16#fffe -> check_string(Rest); check_string(<>) - when C > 16#ffff andalso - C =/= 16#fffe andalso C =/= 16#ffff andalso + when C > 16#ffff andalso C =/= 16#1fffe andalso C =/= 16#1ffff andalso C =/= 16#2fffe andalso C =/= 16#2ffff andalso C =/= 16#3fffe andalso C =/= 16#3ffff andalso From 7aab732346a86a2ddb69b0b7b8e8d9aa780dd913 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:18:39 -0700 Subject: [PATCH 46/75] remove export_all flag --- src/jsx_encoder.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index f6c0c2f..1308a6e 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -24,7 +24,6 @@ -module(jsx_encoder). -export([encoder/3]). --compile(export_all). -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). From 89fcdac86a83ac3a9441269080caf00d25ecb336 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 20 Mar 2012 23:58:22 -0700 Subject: [PATCH 47/75] whitelist allowed codepoints rather than blacklist disallowed codepoints in jsx_encoder --- src/jsx_encoder.erl | 56 +++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 1308a6e..4ee0fa5 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -139,37 +139,33 @@ check_string(<>) check_string(<<>>) -> true; check_string(<<_, _/binary>>) -> false. -clean_string(<>, Acc) - when C == 16#fffe orelse C == 16#ffff orelse - C == 16#1fffe orelse C == 16#1ffff orelse - C == 16#2fffe orelse C == 16#2ffff orelse - C == 16#3fffe orelse C == 16#3ffff orelse - C == 16#4fffe orelse C == 16#4ffff orelse - C == 16#5fffe orelse C == 16#5ffff orelse - C == 16#6fffe orelse C == 16#6ffff orelse - C == 16#7fffe orelse C == 16#7ffff orelse - C == 16#8fffe orelse C == 16#8ffff orelse - C == 16#9fffe orelse C == 16#9ffff orelse - C == 16#afffe orelse C == 16#affff orelse - C == 16#bfffe orelse C == 16#bffff orelse - C == 16#cfffe orelse C == 16#cffff orelse - C == 16#dfffe orelse C == 16#dffff orelse - C == 16#efffe orelse C == 16#effff orelse - C == 16#ffffe orelse C == 16#fffff orelse - C == 16#10fffe orelse C == 16#10ffff -> - clean_string(Rest, <>); -%% surrogates -clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> - clean_string(Rest, <>); -%% private use noncharacters -clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 144, X =< 175 -> - clean_string(Rest, <>); -%% u+fffe and u+ffff (required for otp < r15) -clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190, X == 191 -> - clean_string(Rest, <>); -clean_string(<>, Acc) -> + +clean_string(<>, Acc) when C < 16#fdd0 -> clean_string(Rest, <>); -clean_string(<<_, Rest/binary>>, Acc) -> +clean_string(<>, Acc) when C > 16#fdef, C < 16#fffe -> + clean_string(Rest, <>); +clean_string(<>, Acc) + when C > 16#ffff andalso + C =/= 16#1fffe andalso C =/= 16#1ffff andalso + C =/= 16#2fffe andalso C =/= 16#2ffff andalso + C =/= 16#3fffe andalso C =/= 16#3ffff andalso + C =/= 16#4fffe andalso C =/= 16#4ffff andalso + C =/= 16#5fffe andalso C =/= 16#5ffff andalso + C =/= 16#6fffe andalso C =/= 16#6ffff andalso + C =/= 16#7fffe andalso C =/= 16#7ffff andalso + C =/= 16#8fffe andalso C =/= 16#8ffff andalso + C =/= 16#9fffe andalso C =/= 16#9ffff andalso + C =/= 16#afffe andalso C =/= 16#affff andalso + C =/= 16#bfffe andalso C =/= 16#bffff andalso + C =/= 16#cfffe andalso C =/= 16#cffff andalso + C =/= 16#dfffe andalso C =/= 16#dffff andalso + C =/= 16#efffe andalso C =/= 16#effff andalso + C =/= 16#ffffe andalso C =/= 16#fffff andalso + C =/= 16#10fffe andalso C =/= 16#10ffff -> + clean_string(Rest, <>); +clean_string(<>, Acc) when X == 237; X == 239 -> + clean_string(Rest, <>); +clean_string(<<_, _, _, _, Rest/binary>>, Acc) -> clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. From 23a2faa1bac51720d28a082ebc56e930da6af335 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 00:00:34 -0700 Subject: [PATCH 48/75] typo in readme --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 89aa2d8..5256175 100644 --- a/README.markdown +++ b/README.markdown @@ -24,7 +24,7 @@ jsx uses [rebar][rebar] for it's build chain and [meck][meck] for it's test suit - [converting json to erlang and vice versa](#convert) - [formatting and minifying json text](#format) - [verifying json and terms are valid input](#verify) - * [acknowledgments](#thanks) +* [acknowledgments](#thanks) From f6089a0892759054ca0645737eba38b0f64b5674 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 04:34:33 -0700 Subject: [PATCH 49/75] readme updates and clarifications --- README.markdown | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index 5256175..81bb529 100644 --- a/README.markdown +++ b/README.markdown @@ -74,9 +74,17 @@ when converting from erlang to json, numbers are represented with their shortest #### strings #### -the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters and quotes) and that are encoded in `utf8`. in the interests of pragmatism, there is an option for looser parsing, see options below +the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters, `"` and the escape character, `\`) and that are encoded in `utf8` -all erlang strings are represented by *valid* `utf8` encoded binaries +this means some codepoints that are allowed in javascript strings are not accepted by the parser. the noncharacters are specifically disallowed. the range `u+fdd0` to `u+fdef` is reserved for internal implementation use by the unicode standard and codepoints of the form `u+Xfffe` and `u+Xffff` are reserved for error detection. strings containing these codepoints are generally assumed to be invalid or improper + +also disallowed are improperly paired surrogates. `u+d800` to `u+dfff` are allowed, but only when they form valid surrogate pairs. surrogates that appear otherwise are an error + +json string escapes of the form `\uXXXX` will be converted to their equivalent codepoint during parsing. this means control characters and other codepoints disallowed by the json spec may be encountered in resulting strings, but codepoints disallowed by the unicode spec (like the two cases above) will not be + +in the interests of pragmatism, there is an option for looser parsing, see options below + +all erlang strings are represented by *valid* `utf8` encoded binaries. the encoder will check strings for conformance. the same restrictions apply as for strings encountered within json texts. that means no unpaired surrogates and no non-characters this implementation performs no normalization on strings beyond that detailed here. be careful when comparing strings as equivalent strings may have different `utf8` encodings @@ -90,7 +98,7 @@ json arrays are represented with erlang lists of json values as described in thi #### objects #### -json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties. all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers) +json objects are represented by erlang proplists. the empty object has the special representation `[{}]` to differentiate it from the empty list. ambiguities like `[true, false]` prevent using the shorthand form of property lists using atoms as properties so all properties must be tuples. all keys must be encoded as in `string`, above, or as atoms (which will be escaped and converted to binaries for presentation to handlers). values should be valid json values ### options ### @@ -99,7 +107,7 @@ jsx functions all take a common set of options. not all flags have meaning in al #### `loose_unicode` #### -json text input and json strings SHOULD be utf8 encoded binaries, appropriately escaped as per the json spec. if this option is present attempts are made to replace invalid codepoints with `u+FFFD` as per the unicode spec +json text input and json strings SHOULD be utf8 encoded binaries, appropriately escaped as per the json spec. if this option is present attempts are made to replace invalid codepoints with `u+FFFD` as per the unicode spec. this applies both to malformed unicode and disallowed codepoints #### `escape_forward_slash` #### From 978e75887aadf9b5bb65c73f7de000855e9356f9 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 05:19:47 -0700 Subject: [PATCH 50/75] corrected handling of malformed utf8 sequences --- src/jsx_decoder.erl | 61 ++++++++++++++++++++++++++++++++++++++++++++ src/jsx_encoder.erl | 62 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 4055b2e..f70e0f8 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -334,6 +334,20 @@ noncharacter(<<239, 191, X, Rest/binary>>, Handler, [Acc|Stack], Opts) %% surrogates noncharacter(<<237, X, _, Rest/binary>>, Handler, [Acc|Stack], Opts) when X >= 160 -> string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); +noncharacter(<>, Handler, [Acc|Stack], Opts) + when ( + (X == 240 andalso Y == 159) orelse + (X == 240 andalso Y == 175) orelse + (X == 240 andalso Y == 191) orelse + ( + (X == 241 orelse X == 242 orelse X == 243) andalso + (Y == 143 orelse Y == 159 orelse Y == 175 orelse Y == 191) + ) orelse + (X == 244 andalso Y == 143) + ) andalso (Z == 190 orelse Z == 191) -> + string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); +noncharacter(<<_, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); noncharacter(Bin, Handler, Stack, Opts) -> ?error([Bin, Handler, Stack, Opts]). @@ -1079,6 +1093,51 @@ good_characters_test_() -> ?_assertEqual(check_good(good_extended()), []) } ]. + +malformed_test_() -> + [ + {"malformed codepoint with 1 byte", + ?_assertEqual({error, badjson}, decode(<<128>>)) + }, + {"malformed codepoint with 2 bytes", + ?_assertEqual({error, badjson}, decode(<<128, 192>>)) + }, + {"malformed codepoint with 3 bytes", + ?_assertEqual({error, badjson}, decode(<<128, 192, 192>>)) + }, + {"malformed codepoint with 4 bytes", + ?_assertEqual({error, badjson}, decode(<<128, 192, 192, 192>>)) + } + ]. + +malformed_replaced_test_() -> + F = <<16#fffd/utf8>>, + [ + {"malformed codepoint with 1 byte", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 34>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 2 bytes", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 192, 34>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 3 bytes", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 192, 192, 34>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 4 bytes", + ?_assertEqual( + [{string, <>}, end_json], + decode(<<34, 128, 192, 192, 192, 34>>, [loose_unicode]) + ) + } + ]. check_bad(List) -> @@ -1104,6 +1163,8 @@ check([H|T], Opts, Acc) -> check(T, Opts, [{H, R}] ++ Acc). +decode(JSON) -> decode(JSON, []). + decode(JSON, Opts) -> try (decoder(jsx, [], Opts))(JSON) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 4ee0fa5..5cd9934 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -163,9 +163,29 @@ clean_string(<>, Acc) C =/= 16#ffffe andalso C =/= 16#fffff andalso C =/= 16#10fffe andalso C =/= 16#10ffff -> clean_string(Rest, <>); -clean_string(<>, Acc) when X == 237; X == 239 -> +%% surrogates +clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> clean_string(Rest, <>); -clean_string(<<_, _, _, _, Rest/binary>>, Acc) -> +%% private use noncharacters +clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 143, X =< 175 -> + clean_string(Rest, <>); +%% u+fffe and u+ffff +clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190; X == 191 -> + clean_string(Rest, <>); +%% the u+Xfffe and u+Xffff noncharacters +clean_string(<>, Acc) + when ( + (X == 240 andalso Y == 159) orelse + (X == 240 andalso Y == 175) orelse + (X == 240 andalso Y == 191) orelse + ( + (X == 241 orelse X == 242 orelse X == 243) andalso + (Y == 143 orelse Y == 159 orelse Y == 175 orelse Y == 191) + ) orelse + (X == 244 andalso Y == 143) + ) andalso (Z == 190 orelse Z == 191) -> + clean_string(Rest, <>); +clean_string(<<_, Rest/binary>>, Acc) -> clean_string(Rest, <>); clean_string(<<>>, Acc) -> Acc. @@ -302,7 +322,43 @@ good_characters_test_() -> ?_assertEqual(check_good(good_extended()), []) } ]. - + +malformed_test_() -> + [ + {"malformed codepoint with 1 byte", ?_assertError(badarg, encode(<<128>>))}, + {"malformed codepoint with 2 bytes", ?_assertError(badarg, encode(<<128, 192>>))}, + {"malformed codepoint with 3 bytes", ?_assertError(badarg, encode(<<128, 192, 192>>))}, + {"malformed codepoint with 4 bytes", ?_assertError(badarg, encode(<<128, 192, 192, 192>>))} + ]. + +malformed_replaced_test_() -> + F = <<16#fffd/utf8>>, + [ + {"malformed codepoint with 1 byte", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 2 bytes", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128, 192>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 3 bytes", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128, 192, 192>>, [loose_unicode]) + ) + }, + {"malformed codepoint with 4 bytes", + ?_assertEqual( + [{string, <>}, end_json], + encode(<<128, 192, 192, 192>>, [loose_unicode]) + ) + } + ]. check_bad(List) -> lists:dropwhile(fun({_, {error, badjson}}) -> true ; (_) -> false end, From efe6039c53f31329a9085be65675ae946ec13524 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 05:20:41 -0700 Subject: [PATCH 51/75] freezing for 1.1rc --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 81bb529..60cf62d 100644 --- a/README.markdown +++ b/README.markdown @@ -1,4 +1,4 @@ -# jsx (v1.0) # +# jsx (v1.1) # a sane [json][json] implementation for erlang, inspired by [yajl][yajl] From e43a7f63c79118133bc4950318bdb62e9e8d2ffb Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 05:42:35 -0700 Subject: [PATCH 52/75] freeze for 1.1rc and add steve strong to acknowledgements --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 60cf62d..1fffe9b 100644 --- a/README.markdown +++ b/README.markdown @@ -301,7 +301,7 @@ types: ## acknowledgements ## -paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides and alex kropivny have all contributed to the development of jsx, whether they know it or not +paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides, alex kropivny and steve strong have all contributed to the development of jsx, whether they know it or not [json]: http://json.org From 517dcbc65026bb6197407a611c5860a0395c6041 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Wed, 21 Mar 2012 05:49:47 -0700 Subject: [PATCH 53/75] remove older emulator versions from travis ci --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a7265c2..25a09e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,4 @@ language: erlang script: rebar compile && rebar skip_deps=true eunit otp_release: - - R15B - - R14B02 - - R14B03 - - R14B04 \ No newline at end of file + - R15B \ No newline at end of file From 5d12d6262f8523884e38fdc613012b9b803d7621 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Thu, 22 Mar 2012 21:13:20 +0200 Subject: [PATCH 54/75] FIX: to_json performance + jsx benchmark --- Makefile | 14 ++++ priv/b/jsx_b.beam | Bin 0 -> 1512 bytes priv/b/jsx_b.erl | 175 ++++++++++++++++++++++++++++++++++++++++++++++ src/jsx_utils.erl | 60 +++++++++++++++- 4 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 priv/b/jsx_b.beam create mode 100644 priv/b/jsx_b.erl diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e719fcc --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ + +all: + test -d ebin || mkdir ebin + erlc -b beam -o ebin src/*.erl + cp src/jsx.app.src ebin/jsx.app + +clean: + rm -Rf ebin + +test: + erlc -b beam -o ebin priv/b/*.erl + +run: + erl -pa ./ebin -pa ./*/ebin diff --git a/priv/b/jsx_b.beam b/priv/b/jsx_b.beam new file mode 100644 index 0000000000000000000000000000000000000000..cf970ddf11069b74cff639046039032606694ce5 GIT binary patch literal 1512 zcmY*ZO>7%Q6rS;}cXtvy8SmOj4jeWOhqj4hCrzpXmkCvxgoZYP_Rvz~pLp%8v)*;Q z>yQHysZdc`2`OBV7IEmcTnHDCIJM;x3X~q`2@VnBfH+0r|A54M&L)_ZKD}?g`QDrN zc1L^R@+%XB#P?pDoq2gC@EU}WF9AdGy1!8<({kLg{lJ%eYfWmDnp(|r3w7Uf6+dX% zZcVOxwyW4)!LiFq)oV3Mfm(G+L14LR>(HXKN^Zq#sBPCqgW5in<5kb8IK2(8(srzZ z?N&X-YB?ph)?c%NLYJJ%Mj5%uY_;uX^I65Q+*+_AxxtE_J!&X75$*P7%f}oBIqy|0 zLg?Q>IRKv!FaThbhdidA;M7cTE+n}|gft>#L!=}zB8R4Gf?x<)3Qf)AoaV!POfPdP zB0J=C*~PJai<59kHMy80Mp#r$lbd{2iWy>#OJP$r^Wmn(1))pfc1(Y0(qe*}!jQwe zDqn)*y(@GnB$s%T z?i@;--p5fos<}y@gnM92=jmP_bhH0vJik4HGYcaf?jLrs#d)Z4)x6tjUY2xj(EU(L zLEa#z8t>yo3`?P=gFTxxq!*;5F(`(UsZ>&@T1w2BhF~Tb4B51Bg1NjQ)}l2R_(G&v z8G4!%Af`1l4^{R5(lSSBQ0Ei@3kv_sTLN+5FwcNKUXrMm4o&E92r6X+OG^rz0w4O- zpmhZTS!gsdZUK4$xFrK(5syb)g{VcGLF|b*`Z13Jl7PO5qmFkAqYf|t=sDt8A9*Iy z(TDdhHoMX3=Ol2{QUJ_LL>$8L*{H`Zv|a3f5rF<70P0w`^T^uAKLZ&AppHFZB$1v5 zih0;K_C!%%v;(ZU3E1BFaJg74A{vjY#qa3kxy{7;zkL4Vb@O|+`|0S| zXFF5mgNMCSw~X;vdQa)u8GV`=z4kQo#ZM1za7voP&pM0V^3iMx^wK?B_dMM*^n(xQ zz;A``3_0c^aBg`f2wD(NzU`p05~~nt-FG_+my>5c*?sG;`FEdt@6J05NNw~bNzZ}v2L||SgjIiYWy53a$q;C6{Spw`a?-74U~AvD5Y>m@x69y z*;*xMa#wvw%dL8j<-7K3u41iQPR`m0td?7Ha`2nYHCy(2u8g!$9*4hR9h#Ua`vC;9 zXuI(LA_95zm=N*)5O#u%uv6@LmStmXl$~Z**j09omDzRn8hf4HVB;*u&a!DX$tL~< DIn^iu literal 0 HcmV?d00001 diff --git a/priv/b/jsx_b.erl b/priv/b/jsx_b.erl new file mode 100644 index 0000000..15865ab --- /dev/null +++ b/priv/b/jsx_b.erl @@ -0,0 +1,175 @@ +%% The MIT License + +%% Copyright (c) 2012 Dmitry Kolesnikov + +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +%% THE SOFTWARE. +-module(jsx_b). + +-export([run/2, hot/0]). + +-define(LEN_KEY, 32). %% upper bound of object attribute +-define(LEN_STR, 256). %% upper bound of string value +-define(LEN_INT, 7). %% upper bound of digits +-define(JSON, 20). %% number of attributes +-define(ALPHA,"qwertyuiopasdfghjklzxcvbnm"). +-define(TEXT,"qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\"\\\b\f\n\r\t"). +-define(DIGIT,"123456789"). + + +run(Set, Loop) -> + Json = lists:map(fun(_) -> gen_json(?JSON) end, lists:seq(1, Set)), + Term = lists:map(fun(_) -> gen_term(?JSON) end, lists:seq(1, Set)), + [ + { + b_jsx_json(Json, Loop)%, + %b_mochi_json(Json, Loop) + }, + { + b_jsx_term(Term, Loop)%, + %b_mochi_term(Term, Loop) + } + ]. + +hot() -> + b_jsx_term([gen_term(?JSON)], 100). + + +b_jsx_json(Set, Loop) -> + {T, _} = timer:tc( + fun() -> + lists:foreach( + fun(_) -> + lists:map(fun(X) -> jsx:to_term(X) end, Set) + end, + lists:seq(1, Loop) + ) + end, + [] + ), + {jsx, to_term, T / 1000, T / (Loop * length(Set) * 1000)}. + +b_jsx_term(Set, Loop) -> + erlang:garbage_collect(), + {T, _} = timer:tc( + fun() -> + lists:foreach( + fun(_) -> + %error_logger:info_report([{mem_jsx, erlang:memory(processes)}]), + lists:map(fun(X) -> jsx:to_json(X) end, Set) + end, + lists:seq(1, Loop) + ) + end, + [] + ), + {jsx, to_json, T / 1000, T / (Loop * length(Set) * 1000)}. + + +b_mochi_json(Set, Loop) -> + {T, _} = timer:tc( + fun() -> + lists:foreach( + fun(_) -> + lists:map(fun(X) -> mochijson2:decode(X) end, Set) + end, + lists:seq(1, Loop) + ) + end, + [] + ), + {mochi, to_term, T / 1000, T / (Loop * length(Set) * 1000)}. + +b_mochi_term(Set, Loop) -> + erlang:garbage_collect(), + {T, _} = timer:tc( + fun() -> + lists:foreach( + fun(_) -> + %error_logger:info_report([{mem_mochi, erlang:memory(processes)}]), + lists:map(fun(X) -> mochijson2:encode({struct, X})end, Set) + end, + lists:seq(1, Loop) + ) + end, + [] + ), + {mochi, to_json, T / 1000, T / (Loop * length(Set) * 1000)}. + + +%% +%% generates a json object +gen_json(Len) -> + list_to_binary( + io_lib:format("{~s}", [ + string:join( + lists:map( + fun(_) -> + case random:uniform(2) of + 1 -> + io_lib:format("\"~s\":\"~s\"", + [rstring(?LEN_KEY, ?ALPHA), rstring(?LEN_STR, ?ALPHA)] + ); + 2 -> + io_lib:format("\"~s\":~s", + [rstring(?LEN_KEY, ?ALPHA), rstring(?LEN_INT, ?DIGIT)] + ) + end + end, + lists:seq(1,Len) + ), + "," + ) + ]) + ). + +gen_term(Len) -> + lists:map( + fun(_) -> + case random:uniform(2) of + 1 -> { + list_to_binary(rstring(?LEN_KEY, ?ALPHA)), + list_to_binary(rstring(?LEN_STR, ?ALPHA)) + }; + 2 -> { + list_to_binary(rstring(?LEN_KEY, ?ALPHA)), + list_to_integer(rstring(?LEN_INT, ?DIGIT)) + } + end + end, + lists:seq(1,Len) + ). + +%% +%% +rstring(Length, Alphabet) -> + ustring(random:uniform(Length), Alphabet). + +%% +%% from http://blog.teemu.im/2009/11/07/generating-random-strings-in-erlang/ +ustring(Length, AllowedChars) -> + lists:foldl( + fun(_, Acc) -> + [lists:nth( + random:uniform(length(AllowedChars)), + AllowedChars + )] ++ Acc + end, + [], + lists:seq(1, Length) + ). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 814092c..37a3fcd 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -29,6 +29,15 @@ -include("jsx_opts.hrl"). +-define(ESC(C), + <> -> + B = unicode:characters_to_binary(json_escape_sequence(C)), + json_escape2( + <>, + Opts, L + size(B), Len + size(B) - 1 + ); +). + %% parsing of jsx opts @@ -68,11 +77,56 @@ extract_parser_opts([K|Rest], Acc) -> %% everything else should be a legal json string component json_escape(String, Opts) when is_binary(String) -> - json_escape(String, Opts, <<>>). + %<< <<(case X of $.->$,; _->X end)>> || <> <= String >>. + %json_escape(String, Opts, <<>>). + json_escape2(String, Opts, 0, size(String)). + +json_escape2(Str, Opts, L, Len) when L < Len -> + case Str of + <> -> %" + json_escape2(<>, Opts, L + 2, Len + 1);%" + <> -> + json_escape2(<>, Opts, L + 2, Len + 1); + <> -> + json_escape2(<>, Opts, L + 2, Len + 1); + <> -> + json_escape2(<>, Opts, L + 2, Len + 1); + <> -> + json_escape2(<>, Opts, L + 2, Len + 1); + <> -> + json_escape2(<>, Opts, L + 2, Len + 1); + <> -> + json_escape2(<>, Opts, L + 2, Len + 1); + % jsonp + <> -> + B = unicode:characters_to_binary(json_escape_sequence(16#2028)), + json_escape2( + <>, + Opts, L + size(B), Len + size(B) - 1 + ); + <> -> + B = unicode:characters_to_binary(json_escape_sequence(16#2029)), + json_escape2( + <>, + Opts, L + size(B), Len + size(B) - 1 + ); + % C >= 0 and C < $\s + ?ESC(00) ?ESC(01) ?ESC(02) ?ESC(03) ?ESC(04) + ?ESC(05) ?ESC(06) ?ESC(07) + ?ESC(11) ?ESC(14) + ?ESC(15) ?ESC(16) ?ESC(17) ?ESC(18) ?ESC(19) + ?ESC(20) ?ESC(21) ?ESC(22) ?ESC(23) ?ESC(24) + ?ESC(25) ?ESC(26) ?ESC(27) ?ESC(28) ?ESC(29) + ?ESC(30) ?ESC(31) + _ -> + json_escape2(Str, Opts, L + 1, Len) + end; +json_escape2(Str, _, L, Len) when L =:= Len -> + Str. %% double quote -json_escape(<<$\", Rest/binary>>, Opts, Acc) -> - json_escape(Rest, Opts, <>); +json_escape(<<$\", Rest/binary>>, Opts, Acc) -> %" + json_escape(Rest, Opts, <>); %" %% backslash \ reverse solidus json_escape(<<$\\, Rest/binary>>, Opts, Acc) -> json_escape(Rest, Opts, <>); From ab3c235fa6686ffab8198e4afc18a1f5eb5b5aca Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Fri, 23 Mar 2012 06:27:34 -0700 Subject: [PATCH 55/75] remove precompiled rebar, test on older versions --- .travis.yml | 5 ++++- rebar | Bin 128971 -> 0 bytes 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100755 rebar diff --git a/.travis.yml b/.travis.yml index 25a09e8..a7265c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: erlang script: rebar compile && rebar skip_deps=true eunit otp_release: - - R15B \ No newline at end of file + - R15B + - R14B02 + - R14B03 + - R14B04 \ No newline at end of file diff --git a/rebar b/rebar deleted file mode 100755 index 5703b81607a8bedca70cfea50709ba241b6ebeb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128971 zcmY(qQ;;rPtgidDZO^uC+qP}nwr$V0ZF{zD+qT`k|FzHA7prPqWTa}`y_JkSNr(yQ zU7elijV$fxP3_zWOr4FLEFD~+Nl6I_XzlEsElh1}{>QMib8vNmR*(h(MFju=kN^&4 zBLVG;M#eZ~0Kfwe0D$?=Yi{ad@8CjbWNK*Zt7+|kz3RGi47{hCp{`{>s7NnOO-o`0 zrlbX(FQQQBD~T9VKu)9ZpbZL1Bbh(|FivkQeiMvTqTyB(8LhLqUUM=fk2} zzbei-Z_e#pbQ_V}j;xF;<6mI2#4KsA}7Go*WVcbf!QC;QHYRRCDWr;cA?Np>q5#tf_)S*7NGj7)E z9j#HVIFv$7fE&8EHhJIlnPL6a^9-fvf+V}-LC2a6%) z>DQRD8Zh?&15G+dd4wC7`ltfl)TxIFG~RFHI^^HdJiI_X9ORg~)c7tYwP6?x-w|_B ze9|`hJnyvZ0J=38T4~o_zV`55)O0}v#;*Z9g9B)_b!=6d4?v(ZC68t$&4Dl{Z;m9a zp)z}HO1P!OEK;!@=F!W6cm#(**)y#*2(+L{fwdK>VuMee6s6UEkVh40p)*)cgzHo9 zQO65~tEER%GMb6&`(Dy+0x3>JbKAV)G1H+JP5hXl-Ix zb828&`CKs)q&2!CB7ipeYN0x-z=qJIt3G!=6wHFA%wWYQ69EB#YNF9q= zx#tfY2rM7)zG%K8Hn!czNZd5BIn#Y>cIiyFEi0i6$T*+^GQ1{g+# z&X`G5vsh)Aguyc^=i+NqL!Kt*(uaCGd8JI){@h3VRC|$kF+K;T=M8fg-Sa>vS%t7M z`Uu!;f^^@Hd6_~YBw11b6jKB~8yR#@vN8ebgOqV!%HbP1-Gi6#11vFzumh7b{ZXa< zUv9}0ONsDRXsJwSgdj3{gN`)SE#oSiWf)SxpH>SJ2CQpOsHZ>5*gp~WXW7-`F%5nV`9(DX5k>+ zlTa~Mv|%C&@b(m3aLJqFB?rjQK|Os)2$OUINk=4^0K$4;vBxFG`op%c(>H>_l5-32 zNd}tM1JNxsbg(IyJxDDQ6U>W})jUWA_IPrUM`UCsf{wMX^8Q=2GL5Az7FN+AoMdWn|DMe?VuYakjyauLI$HJr()Q#(NA7?+c3;SM=T># z)FY0c4|TDeW)9fWSh3DOy8ui!xEw|7|7Kpym%&A##I9IX-IQ`BIWLPc%D1DCX<4G_ zVM5mxwd#PHci1$iNr(Fsw(6Lv<4;Ko8=t*E+hiHei%ByDv16$KNDOh(;4M0wR4RkG z1+7q~Bw-UnI8kbX#&F;aQyjG8M?j+SVJ~-cS@Gt zp#J^^!slrG3*L!sZ>IRU3}S7tCx>p^x+0n31(@rebXv0Lf!K3|a4*5OH&i4E(sq@< zAc^K85Mq(ku5DOKgw+HZt|5aKx=R;U<0|r?R=HCv-^+wvs)K*#_H|veykCe4yE-G+ zx^Kl?y3s1%&Va-MUZC5x)9T%B_9;^6-PAz69%5gbXoHbj5klCk0UE{9QdjRGudUn( zQ0)oqxX?9MzlB}0!l>E7wqBOz+!XTY!quS02wc}`*3lhy?O5Z}_uz$lV-4-<=l?T} zEmJ&~)HhH0jsj(@H*Q}aEG>f?v{##;v9uWZ*^Va%FYYMwpOP+8LpADXN`yJki(39j zFPhhlL03w_`6eXvSLva&?3|LJ&5Og#f`pSq8#SrugQCRg>U430N1i7@q85=yioGvG9}BCjnEBB9vYsFVqD0FhfvRQ_2rIxLL7 z9AR&u0P2E|wku2wtp9fowduAXwm4gei-Z$it;E7D+YB`)QNMl@b&yjpF4|RK@-ina9`YEd@oCc#5y?k^4gd#B z48nSy3u^CBLriH($Kw)&enekuFyL0U{IGLdSaFZdv0^TU6M4;~fZbCE@rYRC1N6m9 z(-NTiWCY>I?ger_?dL}lxz-24S4ZRlA_5yEsCV>(ML9ol0sYp1GXMZ_0fcM=7_gTX zVHbG-0@wqH$QPp^fW9tOGC zy`TcG#deT^Il4g}7sX&=3!K&85MP|^?(Zbn`u27keYN?1>woy&=wDMUd-iSn=CVt0 zciMZ%45-7L{}P%Q<~F7(IHdawg*$*uR)X8nhX)Ci8%^{7vZBLP%|VPxO`G>X;{lzD z^nV-Fbb9dSg9W=cX^GP*J%>aj85V$$`2Gy*2Sf5R zVDA^veEfy>b+`{affaeui+J!wJpR;lp>ID$x(lZ1oGryOp8sZZ4HHG+K#|R~fY8_l zUWpgIcdYkwTKQl^$5k&tSdrHX<%DC-HkfxoF+MdQ4|zj+d+1F3=jaE1l~W#j9n=7^ zUuAby+%xXCEz)H;Ar~|d(~RKK7INscHFCRb`jfb#LF6u`d zom+n`aF*A6&4bpzTe<$;8o6L#&ONk4vSWu;J1pOH7B9mh&{6NmS94%~M5Y?LHz%tU zChNo>a<;XVmA34Uw$pN;Bg}CF9ex;CFN2&n&$}rCkr3r$aEeur_k#FE6WwcEs9o;5 zMv$j^@4QbYRPO2#U2LD?z_Im_3T4+qmeEKK8FDQd8v`^uB_=F=beu-8Q87IK7hC82omX2!p0z^Aen8A;x@qd7dR(T53^kv8RyNNt#V8s z;-I!JfD6(da|I>RA8{iea5&jcnuijr! zjDHQsQ&yn=Wi;e3|;fvG%;!v5ThMs;4PMO5Pn#G@nH8R%5)jwof78tDsI zUHkQK>K`~gCebiaewKNLp>_n|Un>-1fh$=Sp+`9Vw7m#oj~$UB0?cp|owFf(&T$Te zaSnUZ!wVI}oS&zH&2w04%ue*s_+}! zU*Ww++m6ma_7G|GDs=omOucA$mNRKa^I1cp{v({E{XeYWx0D#y7aGZvS{u5E+gt@aE(Pi^ZCqjiODq)l-EwM=7^#tU%QF{dh}uRFL;>mqXkBba2~6(XFaEn1oAxdGTtLtIlWOouK|=_B3;3N-taIivj~LGAiJ8Nqd*xWR(hM)2JWY zs9aNPLH<*~WGJqI^q-wM~V!mnA#rq0po#{GqW;~n!kkP6|3221sb8qtox-}F0ZcMBonp#M`v znwR%EBh74VKJ&w%H}O~4diRdK$MGG*S}xD-C+A@dc{bTRu3O#D!o_@--hc5|!6JNP z?dy?#x6b2t;qf#Q#NYiR>31{I7(&V4%T)PwSok*r*krxu)c?imq^)27+T%E^?)E$J z^s()i>s|WA_~x9w7n1i?vh(}x6QO76^Tgl(FnKRso9Nf`b=UvvA#HTB@7`8-_KND% z!gd-kvaEkkc>X5(>we*GN%|_wY!k87_Jh{UUUOApK z&lko0K(fez|0Zarjhi-}t-br);iYEF;PH?*Eu61-iEay zB(CmoQqUm88z%U@&aGR#H6O+^h`S)&JeUT0a=h&SI6HVt)BYQc-1+{ngJvJE2qf>e zfJv)Po5@2QCu=5cY??$ZZ!v77wyRMa7Zc0aU}A|0+FO6sS*}&|NDpc1@c`TUV}!Lt zPEOXiOq{m;P@G7wGK!VXrb3|zuZ?N`2c|^Fk*V%Dvx6U6oD*~M6wChh=cu-~Kk(@0 zjibq-X`@$jySu}=o_C{{6Q=^I0Hoo`z)Ajh$MK{yF^Eu?g=q3TL)VIm+&k;PPJKGi$5Xu|L~!Kv>d zfT@P3JX8Y9&`#XU8TqECTOg-^||<}27q^Bv>3ZK97#pG@=EKUrrX z)keZAnWKZMgRFzCgRp{ZGkM=cohh@Mvx7cU?neG3S<8uov-AtUY475U;TPe5V=EB> zg@Gh^06+pA0D$`sTiLogyBHc7l~o+lq(_lVt}P{(lq>6*lAZgKl7z^< zdA&{tF|cli-S|Y3rM@^+Msohrvi=)4qA{1-g!9w!+w<%G``hTRn{|mXxu%R7!S`E!WRhtEUUW36DAyhz|A|-VJxiX0r zPui05^UgzKF~vhpNmb}Xu(7|7;Z0vNv!vWgNc5z0qB8luIhvG!qhe#6sNrxFPXo>rMg##l%&^PQtw))UJa`xU zgvo)+ZdQLvdn9b^HBTP-pqTt~In_N!)u_kce%vw=K*>hG0g<@pmzN78s^=10VlUxhMy`=40j{(HXGC zYFYUWkD2716jHnsT9I^aP3bzhJHH`d)0T z54)|%F~)Dy?3WZj^j!02?Yr*k*?*5zHX-|IaSt``aY8#x#$oVKnGJ{8-C*}zPKpT@ zf-_=tdDF+1V=gKNhv`@XksQ!#G?~-@2R~wRNM}LOP`49;2B?r!f>u-zEv92pe_{B^ z#|pvqAm~3GLIIN^QiB+4LxZK$+;;;ydTdKjLk$7TiVeDa;8d|^Db*0S(un?14f#a4 zw4g1~29sYpnl1xzOhoMx2?8HphKEF59xIcy2EGE$SbOMmsLE3WFm`7a4M5c38jfVu z8f#a_QaXGD2kwm3yTdT4I#8Dm_dQM!j*CW!`8Xs8=n`1oBH*uS0+KXD{l$Q4-ENt9 zL>|c8h^Ay?21?NbQwM(_4oFJj;XNQ+X^A^jiFBeLDp0F}I1&yWN*uey|1#iPK!m;l zNgqKcbq8tEAnPpwp-_sJIV*1J`mna19b>l86i+AxM=_Zk3)ReIq#a_HXkXU(W7j(* zBydgO5+P!-KmzhNrP)N@fqd|u!es%Bx_|=^D2B){*h%Lv@NZl=N*!XEzz0cT1BnrF zQnXBJ;v$|?Pi*&MU6HAqrBIDb1yBlLApI!Ifcc^P zi3+dcuzitsY1OAoNuAZ>92Ztm<}*7~X0+3_q_ zem>dmN=F@>fc9&%lHLfc5}ZKybBxHQ^(Cl6^RFYG1PJRbYwW>g!bfa^%4JDR+bA?{ zgV2@wztgh?EwK$PC{q>}zN9Jz)BA-C-WJM1rKzA)Q508aiZmpRh}#C*Wal{&ROx~W z%Bni2xh$JK)!KL?z^Z<1=jN;`3YzZ3>cOvWEy#*jO9kjnQ_yX3VTw=h(cx`2>Y0zC zX(n2$30LQLY#M26Ts&(d(4tV_YqH_KrQ77(C!&(UsMHx(ZHSW^l)=rYm}F5-H!PiG zY0}8|Db1|ynx2A6S1{a=(dBBt+=~X*fB1N^@%1`cnFoiIW3A8C0sm`HoKg5GdPbyj z5jLvCun{YP%))wB4l&n~kf<7~LoChzRvqn^3&-wVAfl@`1JiRjeT7T!W4RATDwt{q z{0ufk+8>U?_c)zjeve0*wJ7yhKvKChiA=!=WnmShvkof%$@?6v%jN03JpKdR($4Nk zOXJZ^a5w5ZOwp??gLNZ`I!nnT@4|w>)|qwf$$aAp8>5{j_YzQ3WjMnn@?q*2^oux1 z>xP9kp%Y+h)**o8*xtbkkI=Rfw4xwCd8Rwk4sRp-paKEvK)Y?5m`!K=+xqmw*4a0` zCU4+h1%?FORKmWSZZb7pGc<8BsgUh=m%%0h(8(umzGvfVy*#5v3<8%h6KBN$h1u-# zR520k|5KY>vP-%m~*e9~#qisJB&z63V0dVc1oZ~#Vn8L2vk-Vux5 zEGSOc30bhYHe?%ym$W6jMdpDG{!!T>8+Za}tFS<9bJ+HPTMbTk?m=I=-e-G09ysQX zn9w+kf@$+<-kZU7A=Ws{9Hfyp9`Fx6&l#kVHxB!OZi2e0y-oty&X(ftTU72gHk|Pv{3GvJv{BA=bJg_81<0#;gzpERjnV@l=_FBcm{BBqwM{ORYh5ipC}>y8KLLFm_^P0u%&76 z-}=6h?<)JV();$esp&F)6`qG9sg2#A;`WR@@9TWwx<@pqukdMmlX~>N#cuS;an=1J z_tD&5i}U+<4*PJ|o8IdsByY+Rg1l;#2 z73VztXsE9pFW%v*DzEo#e!K|c&yF6W+Lp`73D~K-{#E9LXXlfzny*v^UJmc$68Rsd|AGa6 zs~$p&?uUiucatuCT(KYTf$gvFG{nriy$@c!Ngw~9-{s;g{SW=)d)^L|9B(38*6Ygt z$KdSm-bkQ0zt_>!=mM7EiNL6_>u6s~WrN9)M%ry)5kGgQ_u28cIG@h%)wwFQ8~#UE z)b|Yr-<#nx!tD3Y=jqHy@0#k>rElZ@(YyN&&iC7Nxc;xl;NdiBblofmH{W-BlPZE9 z*NbL)v-9UumHf}q{U$#6Z}#VjJ^%UdcGtpPyqq47W7Es!?iE%ilGipZ57y<5`%mz7 zWS)PTyWh?6)!uJj9r3tY@uqHjOz%tn_P$ko4Tx|`@y^D_FLo@!QEbR5V zlCy83WJ=y{r6pBNPWV~Y7?)Kn(^alYwj7OPuIKT>W0!rk?`+R=g^2k|WP0AZDA4-8 zq%BK99OL_UBvgaCs*)CwUXQBcVN@se92$Puv_IR^I>lD5#KLZ^k_#dCgu_U@O~KO9 zkcfuwzq#~`r9#FzbLB82$4;ZjSjHC(9wt64)xxPQd7mPhu|ma>ef+bHIrNL$jWxcJ zqvc80XD44@w{qm}=BJ-GWtVqj3@kHVs@O6%$oF7DRxmd>PveL-f zwdS=DroX(PbvtbXD|Z9+h%<2XtXtBgWXlmuBPpx!#Rl58QpztBEtITERaC-;%X?=2 zxHXDP_qsL0l(D@sYyqz#^DVk4h#USLjwR%{}r!JrbdQNbcPNNscN$Jn+&i$pX!icPFM%Pn_g1$ zll4D=hHXR)Cebs^j7g>O)uHphe3Wc|S&&Mm!%2vCizuVKrmTiH5NJ28fv~~RJ2tjw z^=njsQ|yu81F+rP`>J_wb-52yhlcFbsKIBkPqqhfo&66+&+tzOV0{*4wlGsbUX`j? zo93Lm?A8`Z{=upJUH5sUU1z^?Jn@A>2K%gK-RTx%$bzQ}eF9V@kR!B#n8-2uh=kG^p)m>_p#b2o{jd2zV#Uao)0Qd`KA>x2*bNimL-rMf~Hf z%cn7@z?r{3jLpyP3vQk(iB1A*pM$ZCoWpwo=kyHgzoNkq=|X4nYh}>$a@%X^u!`_? z$VK1}usy}LGbGoYf)MFGioTW+Us(PHQ|nh{T{!F(SU2b0+DC?4A_V6&ER6CEO@w%& zd4b`+*cs;*)pl>LDU#E&HBO*eb(ll~T9@DMV)oOjT|brnqfW2OgO1K^2{<)b>J{CZ zX@@E29@jIoPH&-S5rNNLjK_(yD8fmxsl@R>>Opq$R(;gqFgHjj!kIv;R-tQAt8}Po zv|mpXhY|dMXd%d8ea~;Cq2%wUgy!p~JZJg-Tj+q&Tu6q3BXk7y^z2jOmXKwQn)W9V zm5(_d$|7kE`r*5W)4P<#;iu}l={F6Mfw30yiOHx^dmLz{`uz0ex%D09f2&Obh*AOu z1puJI1_1sO-2bn({~7QJ<%H2dHSJ>NhT(2v;AEH-6~NJCQ3Fi_LlUt-G(09M1m2l* zb#vE1e_5FcN>Wo(%OIcJm5$TQG`0LLLD#)Hs@rC!j=eG6p=yZ^dr#iTbK9|ZQ+VJk z3!Y_v^t=6j>Dh%$RW3U<(s241ifW`ZCG5i(Y;H5aTtP=dy%tiqMBD0xWW;=eAl3$L z6L%4d5{%z-w$uibmpHUvZU0WB{mEB{MCymvPeFwA*n%fKdiiX|>)VU5K;lUni{R_O zL9z|Z?;E~${M4C`a>^RKwRY@!qGL31_WW||FM<13CW=fU79d|EeOi%L&o1Edk!u*fM#({vo8Q?d`X`{G;VwRQo#--VKPp6Si9s@d z;sWvf6M(5=lpY}fVIm#{iax(uVf)zS%Vjcg*m}90L`%`h#UsQZm8jChkdTx=CNKxD zm}GGH49!UPVR6L0fDe58-B)tR?LJ$Ehn2uTy&{L)Ag}{Ysx205$TTnvAv%$=|$B+>jGib}Zl-LKH7^dRVQ#*nvo z4eCa@K<#yHqlCfx#?6G72KyyXtr{)e^!N*2Wal zEjfBuv%#&4$z(8B|F2JYbh)dAfZ%r75XOUAd29PrBMAO?WJBi)G;|-@e!lMOJ zxQpO}G^LHxD+w<{c8B|*C|k{!+y9FGf?^74O><>Fxz_zq{nLHW{R5hN@s(orjmMv{ zu5ZVB`ifmdXz!~PBX=4sLNjt8jufa4MFNuIk-C5pr%q<L#GnW}Z zVNwWxDJ-wb7A%0BFl=ZXG_8Ou&eY8cFi46cG)84zDY;-vBX4HPghjM2g%e>E#76e; zg`Iv{VB{EQz)>p@WxG|kEe(*>lO76~V?bm|nQP#{3U007iisHA|B5Xd0{f*t)!l?E)K34rQxOh(>^N@r=dGithBpOM_A4=xQv2WGW$Kg}MQCU)ne5z>S+-0k5o{8uMB|)O6m?Mx zPEM+1nUHsxa4EDP848b-l2nkJv4SMxI3SCLfRq^7g)g&40WcQmw&(E;sG|(CIA@8`1-gKp zDvn@FBa}Wg+}N6+b2{M}xSoYDsDPSadXONwoTUO0nQGY~6X!po6Wr`Pk6C1q38@9G z=Qm$piSDI#_FMyctKXDliMO6#FAZ zj~N$=t8?kXG*(YD6gjg8^*AEwlSMWQ5GIJ17{ZeU)*FuDD~vK-;3&UPRlxIOaFd$WI8gKwB1b#3Pw}oe1Y|!=iSVf*(3EC`mJ!{L#RQ4&&4WBvCv$GI4^btN;oSyNv@I zYU2oFIW$R?O^Qtl@_Ys9BLkC6!!t>r-u&lxZ>CcIWE>06X+)u^$|mshnTp;x3v zP|(zv0tIG8jyY94$tY({fTPziVph8`+9C2q;kwo}rOM@#68UC0B zR3wJ7;OFi=!HqB(>v{4`r$5s4pY|tM2AJW1;U-F^BYs3)%-Mn-J(Idn%B2Co$)yKz z=cXN*)|K(g$AJ@?UQ6?fVOBXI-`WVn2@F!5p$ge6P*o_RWKG(rA*$Nmm%Q? ziWSMMndhhyK(vAsF4Ul4LzJ}gXS&j?z`Sv)=1zorWnklV$OGcG#7|ej7d=@#7&-HD zLvX5+B-^{5odK%64p~rh8OL)|kOvk(B~t)A!KQUV%hJr;pue_J3pT7hF0^th`B{Vc zQ114$?PUOE0l>3Hjttj7G3H{Ai_}BXWH|-45gB>$iBj^r9v9xTtgz%~^{52^@*=A0 zA{MjqxM2KrTbMJg5W6{$XP8WDfE{)K+=RuPP`)L@rsRMB{Gj#BX%x>1Hkp8}6G4(1 zLue~-h9%H__K9L_mUTg8WkO!;O2aAY;?w$h9Q~1jJ@zoL%>lhxQN3AJC5EQ-+|Xp^ z`Evwn5V$7PI`h*-XGjeOTw{gC)4QHC*4GV#jU|J>rm#E?0*6z=kI2D(6aC}whBWOK z!tm6faUods0BBHDB$7~}4M~x#{V7biGXa6-IbE79)oX=;3NtN{pD2n+H;47WBtGQ(Ce*N0|JnhE;4*}At^JT7 zGSmheKq&FacyFM|fo=k?Z zil&LVG-xwnsmv0m_0|S5q0>}&$HRpeQ{k-WX=mfuhYbjm6oRBkO|i-yw*hAH9TsSy3K|GiG7ZsA{$C_dj`29Y)ern%jc2e+2oiiuZlOr0Ul{-imwt$fevCq}nEbFaYo=d*rsH^>nvfKgggT#% z(QPwRIoYl&Q|tZCW%c*xY{k`fxesla+|^FL_pqRQ&rqxWp|lUt!^O+k9JcAbVS#WSw4fb+PIcTSjZ2PG)|**c+`mzN@$9 zLg;v$={{>yquJ}cJX_|=&EGXXNyEp>%yvUJ+xo&-LJng4AG*=A^8KhM{XrU?(K)}{ z()Z)^HM*VJ<>H=o|9R22n#f!b-u-$V*cVQ2-S#v46)uZ|qiij;i{M$v(0z5`a{rY2 zY#!%q8FBxSIk!D>_M`Lo?lk>D3QuciJ_+Ib-C^>5=bwqw`udUunzTNxL>@k`arXP( zQ02GwfLKP+WFfBM=HAHgtZwo@{jMVKzE5dc>f>pjzf2{AP zspY9TI6ULAl=MAh&fYhU=JSLzsQ2evXtwHi+&h6vlDpYn7027V-eQ^B_prQ)@{?Jv z&$445;OnU|vfXt{&84r`;xkps;Wm9p-B!(FBeP+ie#n;hby{1)EYGs5y6A9J8mv#& zXRN6!Tt>t7W_pA?*{N%;cDv<*@yYq~^0JRm{d4%EPxdaK%ejcdNLa=A)1aCa&;RuH zUWPnt%N8G1I9q)Fp?-_iHf+AOfjY)<{}?)#=i2CKF1B*mbg}Zc7s+kY3AuOe2m9yU z`7N^fqK(X-;;{*zVpy)$Ubi8>{e6t2#(7h`Mt(i6dO1E?6Z4FFfbEL5=d>ifXa@)B z`6?>DXjhkz#Rg})zIJ_9|AJ1^$N%Qu#(b88&+ho444vOV+f63D!|Q^3_LS=;RYG1% z?Y6gCJf7cFuH!J!6`M{XcaYg>HD||&l8cFIziIKitu-#IyToSf0-rXkueW>ZHP8MP zg4TLvxtEX7&eq5Bs}I575Hs&bbvvZ7$@+q;%(sc>&{K9C|Azghp*8#9C)@pIu~irX z|9gOb^lO2p`=y4vp8GnwmeYQw>vLrGFG31VmFDviGiB#d;;h`(R*~;sR!!>P81L2G zPj zGL$NmDC85oG2BLz&tU!z_v_`pAv1hj?(?PlpvK(>+i0KV-%d_P(W&g*5pFX6jgHmi zMD)**h{xwkb-$iHb$MBfd#Jo$0gInlk5_U8yJg!=t)}myWo!7OFxX*oWquBg#fSZb z|EB9)%0|s<&$hcgPeQ>jQ-TioSxMX{O)G|3IR#Mj1 zVf^W63a{MQn5gohc!`GYL%+Wq=!;K7BwC1^pU@YxQ3_wB2JJ!=F76{fa)H=ntg&$a zLTTt*pEhhT?vKEhi&OKup(a+ag$htpa!B9 zhO!Lvj_)G5L|kil$rXVbaVkk%p*k1m6N|rp4ysaU!+I$crL31AQVG$%!Pf0IRPn#U zZCy5`WKNWfD2Xn4RK`%1QB{nqu&PQgD=JrrS-~%hOqYR9NOqLFO-Or|14tEfmV_#! zwPdzswxzb^wq;$FCoB9Ftjk-Lq?aa_=&X2K0=*KvLcB6IWO&7R9$N7xB6pV$tcUrnsJh0mT6OJmb|@W_K-=UJho)0Wuax%x!%0w(p7<5^qT|+ z`-%U*k(sS8sow|~0B{8X0AT$;WTtOuWM^iqZ)|VtU}(Q9o55HWvZe}1B>eejLS$iV z^cENo&XTwXA|m?UPfdjxNr)aBRvtpIAD-{-24V5GPhx($<=p%DadOjxyWR5);Gk~< z*H&x8ykG#R-~j69$|M`g(IZDvOn8vh!^Y9?=Y+@{7D@eS0p%M=2z zf8hoTAY=yA*C9b{IF#TsTQg1@V~$?WP1R%7h9gfaZg?jYF_eD45!lDMM*=S!%gycN zfmQZNATrD*7@d#|+V;cRt3C)iC!s;n7~_@n&>@J&MrpRgnnjCyzp`9e`f|Szvc?JH z992N!AdguiFCM*?dxrh%3E)x&1-RYMbsrr$6cj;pMipS(Es;J_0i{GNEfXm|F|6GZ z3un=c2Hc9U%Tu+#8(mSkI@dC_r5kZBM2?y_;$>B-4WMayIfpQ;r5A-C+xkh|riS2; znE1*S@Ux5OG9oikhwoqe3?Z}^>4fhgbex13%*lUphm0g#1|p)^Jbeb&cKGGGs7ofz zxF_Q`UFwCMHO*`M5+xq9R?Wna4Zd?OxzMl z-4abaK<2{sA}dwTf`+7k4KcxZ7eIBkLq+A}usfk@+dStq!6c(<8#NveH7FFXEdbx! zx3A27RL7#@&1l?ii$K2qlMEph()Nyzm~7SLV@HxODeaM`-4-`NN17O!ZZ)2aR9WMa z7pZsUPn6T(!aX0O@^#a-($cAj1p{;b`RY_= zqnXkNZ8U$}@#Pio?ENPim@`TClIOtFS#Y7Y*4E9t_5C?qvTL(@f5&=!?0l~4ERS3tg}p8v7V{Zi9>J3BFT^!}yt ztP(AmAH9tx%DdFBvd7h9$~T{wd;JQi4A0|wt))z#_kKMMx2^5IR(OBMzT3&v>U`-tJ_@^!w$q>T?Y*Tl|V&hM}{~F#3jD=`aHe^=gKzlVY$7@INHoaavUPDeB#g zZ^sC)o2M-8fxU3~I0U$s&{UH922LL1{hW7a-8NV-sJ)q-awJ}E9-QUe768~086k!w zG=q|i8Vw9-S%9+I!MTl)6d*-)?46!k$p{fyp9n~&=5iqCIpMd=79ZNFyOA3FA()2F}DY3y~$4B^8)KzFVZJA*`pyADW27 z8c#+kaq2>!W(qF;RlT1X>A;arS1zckML0{5%EcBB-5ag;vTb+om846Da?(<4^!1!# z|A(aho>BYXa0+^@puclW%{Z(@Xcm+g0)(hy)QaRfD}BX^~6-U(8d zsif()FH_bhG7mCQQzgt=bcxGv!#-T6hD9=7Y1xwIO5-892eLios#By)VQK~EUV3Gu zF*1I5!8QTdUP))~-QxjUe{&^|PVRRCdzRzuN2Zz>n6enIbZQL}7}9G_)2Wb~JhxFZ z!AO$j$fjKQ%vDkmc&5BJ#ykyhZT^^joEP`oEdLwOkivl0iUOK5qAQs$an_o!&G6!4 zzh!%r)|x?NdJn;67*vyFLxoM<%RG=2$~79PrkRX}5mlx+X>J0ewFK_2IM)nr+{lqL ziTXo(<{7~%QxxVn$5cWAD6g-%sZPsTb{QZGnBQ9m8!U;_RMHY$B|w>(*!m<5gueB+o$*6oEK8do()1SKcjTipx@3r{QN~k@lRH$T0V6sEDI~&AaQt%8c^F4_=WxP{W(rzosy(*;Rx<<% zB&1r?GJ~Xs6{IFa#TVp2qcnOZGqdU!a{^mxr_xPG?2M|x#uGKvBXOLZnqM0VfQa)0mUOVq9sa*KGLBhFJg1kh%oi<@U;~p;9BcEV)asR1gVzY< zr&)d&(FaNc;?R>Luk;>pVLOBtH3#?jf4DlQU{Qi?%Wm5~+qP}nHqW+g+qP}nwr$(C z`$WHvxP4#5il~qJt;|}v#+)-RS&>)y65hawYp6(Sz`au1`U#)o+90L1Dy~a3it>}% z(n)Mg!m*6=!-=9Z!!tGh@~HyzJ4T9iDRfk@S3M+LfV0x5!+z57?@AECH}%0_#@h;I zl*sr@XMTM}?ON}xf_xdpFayJm)D9C)_tm13+Ekmk(R97Bb#iOD6JNnVS+8xH#H(Ul zK|xj1>v#IIlX$S0yFw1VdEPErR5HUzZ4@(VcJ`EX$rmm>#FwyMks742>6eJS^Qpl&G$ApL#u)_!#l%$S|86Nn{h?Wwa zH|IRn$wTNA9X`=uaTle)Xj8{ZOdwGz=0jdFmcJkS$QmvCxj~XcNs~twv+mfc>2Wj0 z8tH@sV`tPU($fFe6E((3Z5*m;c?W!xhY>MQ5FQ0JZol7g!wGL=NykDdHhR%A9yhHQ z7^a_6p=EOsI?)m}AMbWV&^ORSY0X~a>~IWDL7-SHx3zPrb-uPAOzJ$o(?)ca{ymVFjgOSW0ii0DKAXq1HcTx-l+6K#qX8VY6`*bHdiq$mPiQg zkDnhs$dzI@BAGrJ2zjwzS#Td8$dAEtC=D|SQXd|LhJoRg3D=0r*wAb|PhD&uGU)71 z*&Rm6sT&H@$2D&JQnE`b<^7CJAlp&fqb6ajNDvRk&8L1L&;SH}nSy~6vVadrezf(e zL(qI!9lR7qzF~<^R`(ZK18AG$-GJCL{+>YBqfQxseV`rw9S#4`yhhdDP(vCfLh2(= zxwDf=>crI#W9R1l`d0{}XJg>(l@?AX%z|#n;`xx1zm=nt$*s%Z0Q~Nl9q_W9hTWbxP zN}TP>yS*&jwk+)$A@jt0cXVyFS>07y8IN1agLlJG-@BjNn*+1g6r$Fd<*HsLm)ng4 zEG6(_JsJ4z_j6aY;a=&S@3Y&}foR*`bFBP)wWm_OjqlmYK-Zk!8=v#)@^3ND*KEu8 zyHHOLUY}AXkJ9r)zMZf4OJClHg4;oJ_x*{FR8^boFORA_7te!_>-&>f-R_tEiEp!` zm0>OTAEt)8-CN4vgwEarALl3Z^_;@w?dqQ8!cVjvOP)Uo({JE(e6OLqIbT29JEyuf zUI%6u-I2Dcu-_%m@4LNrx51vjW}oY|xg)kWzef`rslM+kLxB}|%Uv!MP6xF)2aUl} zp$`%*BXpde)C-S`Hjq*bQCF+Gdre3hv{4TtgFeI7eJ78#tKe2E5T&^0K3 z#iTvskmqxl)Oy;z3RXWhfO}rIE^D3XXk%oKPWnQx-ExLKA3#6LCSYz*kggsdq7kV3>pRVf^6-(AX@*taIu_vHO0Ot1&5sB!!GJDONO5Fa*Fba@yU^$qwq+_&Y6FJ z@Cf71qI z@&@7w{sRAZBjZ4M5KIdL0KoiD{DJuYH!{w4|I@{ItHWDiE&t*B)VX@i()PUPkl1V* zXF2_m&>>4*GtW+HLMMyKWVK#o73q`58SNpq`I75#)YPw&1l<5M)R^p5DlI>p-LSx_ z#IQ;V5I3k}@Lys8vl1%X$c>PN5ZNZ316$~EUY;6PUiJH>#*x!bV$4SFS0W54X@(>aUgC4_DTl9$}*N#7~`| z&1bi}Y}fv9RHx*Me^ig5!@#j1z?qdT*m>ExP!u;|ukj*CjU_2r8e*(8(mi$+I(s@q-kLl`?A59ZQzAnkXH|EZPHP6ZUx6 zL91AXzl;cSloON6EeLZ~l;N5%h6jGgNU?{bpg8J3zgd_g(sCl_We)AvBFj-6II<~$ zjy9$oV8W1 zCXBPgDaryfoKpTusSYQ`L5uyZNXuSnOJGPDCyC^TqLtFQS5dBab0E!;DU(Q5ovI8@ zM!`P2ATH|6p)Z-;R2f(f9qJjZU z{3-#-7=)w`voyplHo-CJ^ZFqepW~P zL9A?V7AL1ju#%w@s7h%zjgZ>$!N1&B%(@OH$#w`eOL)0G=Lis z9#$I>MGc3sZj>Uo5E@LOx)sgN*;tz%si+MRT20O#mr13~ANAMzZxv~f zEU*P?T&tg^)?lAmbK+?8T_3j#6YCZ09p+svlC?U7J#M}z$|uOUP-?QD2~D2AJbkLx zB}zRwn2MdG-z@UcR)GGXGnfr%L~{e|YG@mYVlK+r?a2iY5B^{FzAISpHxLXYeetk; zkiuZ(FGRf6ymrlGpK(C&sSqp+6H`l#GSIA4Kx4Y;-UYsjydno-wI(juE;z#Sz$dY= zUs-=R5$)Mn0X0!m1!51VWDn%esgOrXiMdre>3LAjuU-mZr-)qH{LmfCJ=Q*ECr2p- z_%P|tbXE#$JP)Qlm$THIh^R^W9rX*mMjIS@xD2T^g&|<*Iw0m1hMzirE`l@?=IX*e zbLExi1H>s_n6yj)k~5<_MW2}1GrQut1eAG!1blJ9cm?7mQp;yUFWvAN zKy!3@8minF24Gu1`@&eQ5N#V>qrEApW&sip$T+P2Mr^2ZxATAEB~XmUeG-8hS;eaW zq;8nFIa$B8J4JCwW)@vAr>AwZJ)5#;Fm~EW;~GS3L2bjqG)tn^0n~#G?QQKw*xIub z%wSvu#280){WzrT(CC-Gf;}XH<^uQ0`6WqSSrly>dMW*sp*oP&f*~UDB!4ym)>T!b zPfm?>W5zwYU!(I*=y!RaT_btmK13G;>om(b%diDf z`>N}8tE1H^+7Ym|6-|jwlqz zgwn9QHK+pc0;1Z102iS-wgKtYHven@f7}4d3iR&;PBRl*fhZq1l7R{ZFjywFv8s)< zfVIW9J}k3#LsqG3Rul!lP|Pf>0d~R4&Dk*UcfeXhtJ>oGuZ|c{3JQ&ak2xti&k^n= zvEcai{7f;p`w>=yE!=wGA*&8H!pug)wjLrdQ#m0#QbfkKMAhP;=ccUuY=Bd6`WhI9 zg3;6k!(R5$HKuy1j37bVVUvonJTTN08j!3D%1HM~0^FFaaNJ*cr71vjQwck2G80`@KosP$6TgN4+YAr9k!1vxRD3s!!Uy zs?MRad!ls}$oN^i9sc!xK1_)|;n`q1kK749* z*5cngYO{9mj`Tmn)+^CP&r=KXrEzmwnDQj?A%Z~bH9S;;Z|Zu)I0Wp1vz5nIWi91crCv2T zhpSqgCVET$-gH1k%{J76->X$En;4jVU8~9(#lb-@LL${Y;ft$*%EcDb)%QfEJm*JX zw5Sc$br0|gK93P-(wS5z=USt+EG|seuT3(lPsM1q7qscDG^SFZH(E{?4pL<2_|xF8 z!g=#CH5hVT?DZ~s4QhOcm-s7WTVpKK{k)}it9^3UXlY&McpPqDjpOVe%Iy@k`o64Y z^Z9-ZMt-|u;obWFzH@QysNQ-W@2o(F(zfXBxV#Z`e(Juqly2_4Tkj6fKCrcR?D8@E z9?i3rev1jdRk$JSF5dYwvOJVVTMvI6{<=N;Pg$;JW1D~Ny!=X@hUQ#Dm&biinTZkMt=`@3o!Pt_Yr8Irb)L%p9@xh4(e`-nsr*iV)^N9;4uj9` zI*+D+1Joc5ovUNA^r%RXoAYapU?sPpY?f!Z_ajb7&x?PV{)3xl_ zJ+y4`Evos5?f6i^jEw-L^KL~nk5^fQ)6dKdDg5q z(1B?f>|0W2t&e)&O(r(V;nbOMn9$GBzi8E$tk{>}aRHuy(LAT2L5veWhkG?YxRvea7H?Ju!MlmJJ z6h+=eW@x%N;B5n_2lpSubs`sj4?O1DczhUad8RN$K1rs@Ylf%tiIkx!(L-= zu}mw=l{hh!O@!ajzWe=IjMALLFEa-N-4t9JxG0a}JjMu>b)&P1rHh5W&v1+UlLOY9 zg?zz$Xt`V3-{c~rX1e539cRY&Yu%8i@ylVM1bq(deV8HpX@PMNG73F4px1xln3>sx zgr_05A%S&H42VRmLoDnuhPEI#U=#-vZ@?(|5`Q@kdnEb*>%g=w_m@}}=?Ea5&^&=m zgi#1CLFh=;0joWGOCTn$@W|%D+97I7>V|BWr16N|-H%%kmlQre@d*3~g+pd0uJOqJ zLFfV5A%|OxHtuvByIowDP@*6Ka^z8^(_jC8dl*>eGldlXA+n)=hz#RDhYLp=hW|~- z^vtkDFmkusCvQkoU2a|y%s$9Ai3P9IjUG!`%B!=PyR{3=v8AW!zqfrq-EXk7sRItkadPl+w=JO^|DpDljzZIxwty@`r1%mgc+xEPB z@4kMXgskri%{l?&d$+LhxarTiP5?lN0RU(zryjwuS-QEnvfd_9sfM>Hnu#Y`k!x_g zr>QVLhFFFiW$nlJj?GnX%zK4nuxhR!3#?bzCUx?G1qv2`^Z6z3> z4=GDh&kBE2ta6kv{%zs6I>6**0M^lMGIH-h&vueDDX+OoQz0E~Aj^KH_I8=}P#X0G zaea1?R=tMn7IMKEJykixF_6>Y*L;?`@X@RKvA|DijY49UQLaxrpt6oG1k;np! zvp@pE5QPFTm8Y9A6JjrH-G||gBBBg&&oV-34`3E3?MSm@0UQN5w4j`cV#tA`h&k(B zV1P6)w|C9r?Pv&>6biKGc9 z`se;d-I|h2Bl0UNudF1IGK#Dnng-u1^m0)@?P_}Ze2=jg5lU;3;9 zpG~nno?^WK+CeE=H#pQgb(e}>vF6S_oL~mv4FD;P(InUlF*i`wa4%u0JMZ|<{7x+_zq zw%QO@h-OKHm@&-m1G{D2=;KQogYOc0xUgQncaluafwtuh!q-?$-dOMG{fqn7(OnE} zS4pw+sq>Zo*Unvzi#vmPwVPLGhRE~bb3PO@5(Tauh#U=&-MO-Rw~uZ>3a&Djmy4543-kdC<& zFz;RC$9^@*u0e`<5WA7acfmSDxAP0Jh4*$IHMr?*D-OrPxa^Z4%)$#f4x2s(fq`m| zE0$pnBGW8ia02Pn~<5MY>#5Df$4bt=`tG}`WEv6ZHF1K%EXFXT(@xbSV>br%muV*|w z-|IL2H#s{!DhXarXLc?}dw2PSWmM`tD`HPyeJ(s^A{hiVqr#|wSC?0(IbpWR(C~L?%sO58R~}ClZKHPSKv!Rg;h7Fy{|jUTSyu?)u-~(Cgzptc&{er|(}j z(Tv|#9)R3$*08soasQH4VkxsFQwyjzlW`^vC3_k3xbv?RU8vOmDCVfXP?#I5kCT5U zqpaQWJp;TU|M!f?ZRRt(4~iEe#^?W zUX+@VS?DmQeJOU<%+otyC!NDXgA7!GSq+~J{}*p`GXu(8uOWgeP!%9!b){P593Ybk zqSXuw87>1f^pnlm^4$m<1QG@bS?&uXW4O@tx}M64vq0V5>3Z9J`|We<1#NnpdC;cg zI%{kyH`}2aeFy{@a~MO_(yrJ9O(il)U8UJ9T5Pc~X0rmkVlviLIsB2am$vLsMATxj zJekbWWzrE0YdH*=GL?LSd45;1f(ZnnN)>a7Kk7vsC+_5|F4L;$JmgTUOPSQ~*J$ay zt-VdH92uikra`h$S1tOfuJssUtm2t1S*_50uDGwzxn`EuvbQ0_q)w}f!!*ULLPfRE zk|NpuH)UYP1QbL{$7bH7BMreM)uY}dL<%)?n;|lVU?4=LT+F)BM8_<%LgQJm@T>+L z$8J=mXDSWq6HcTBhbgl1Qm|OJ1qhW&x#dc8KDos`mU+5_VrhC*J72p>xn7PAbXs%p z-&(HOTqzv*S_hvAy13G3QD&?xvApT{(`0O$;WAdA@lx@J*?O~~6Mv4hE^T<5x({b| z0c6RVkw?L1h3qNZ=C`~9(^ow=3rMoeWJQ&KSF#>t{QNE&$7|di1+O}A#ACSGYW&o- z$^K8Jk%RUd##7Xkvkz_`AwL_PrvpBxpgT)}QpK>DWZ?vo2$YVXGIANxtFk~USkyU& zOhH-A9p^qmw578A3VX!Cc0g$;hlZ+gL{JsNoLP+0naI(6UClqUXVrqu(eYq~tf8dq z)FgC85~!$rvT}0ijywJC-Xk*^SH5Z@-z|&Z+yPd+Y9wJQB}s-7YKGtv;+aJh?XdYs zo>`8?q%KvJ;(J#X2|NLf6r8!;zd5VZANSedWo{aZ9Tj?)1i}*JlAdcy`6Jg6dKk-hK(>=kTd7}9fH4EE_4s>6&>~&ien?fE-r^zR%9-8 zXY-q+_;Q99q8ak%{uKy1>4!)nLN28k9Nb0#m#PabG-w|Nil9KTGOMT2c&$TH&Al{A z&A-md1DJpkA0I&l39a)|Ikk0T2m1(B#+^hYj|95#7eoqEVU&WD7&mhYMjrncAuzw$(g{p8_>2o>F%B~<>0f%VOUh53mNc;>=+I+%x^ zV49y{NQ-&6ag$VP7CkzE%#m3j-07Njvln!6Z_nxf5Q+KM(^d71A9gd>G*JZw%xx!) ze!`UF2}A}83F(J(H0=%w?ah%7)nG2r1IcqD4R(?j)#|u+<&F}0FBmWrk<8(UOFk*6 zEIcd-;95F{DV@Ysohu4*fRN$Jiia{D2*Xz@*W}C7+arzu zb6TLljB&Q+6=cOlHj@pkEf~Yc1~0>Zu>g3xKun!9{yEXSGyynd17=@?#Gj{&zI(d^ z=#+JPo8z&^#k;oys$hn+W@_iE9rTb?fGt7BEKQuP*Pa**%_kvqt9q3f%~M=FYjZ*= z-^?d2DvV&?a#WMf_zPzdHe-2hz%_swGpa;p*kM#x!?dB3DlJKYOSWm?@Bfok2f8T( zz(;0iMZfN1vqf?-Scap~c=t&7oadw`}Sk4)~Rp6*g({kR0)u2my0NUx&e5Mtr&EQ`|h%44j)| zBqf@g!-+VdlA@cSy%0xLBdXfB(i*D+F8NS#NGT2OwB)F5YO{B#ttTW)Wy9j>9=8}p~(w_a#^q8ukv`Wi$e8%9p$W4 zyy^0GvCyS2axdXH-8Y|e#%|h~UTG+pPeKZ~z_%H1PF1ET#KZ_0o#fX`l`NZRd2@1O$O&&B<{_>43!rY6-@ zn)P|}IxLs_ymdGJ``GX-dvE<&eVnVz`?I{sHgNm$`ekso9wchovx?fto|hm!MlnlroW{ZklBb<0ch^}O0W{mPfr$@{(%x>`LY79KjX!}Xe(>WJ*y z_0~kXj4HtOl6+jm<{fS2msp0^tq5u2#7+Pu&EW0Y*{#Up<(2U@!0Q#QG^!DdS9<#HSi z{_FLtcXRD=njRe;J@vO6rpr!CSO3AFa$mKoV-O-AiLCke5iQH4TD1w!6o{bme4zJD zMwVWU(phw$DpR{nQLma+$|Rg<+G9Kj9=5S5#!rpX{$T4nvh)OcGP1NWbL*yvH+d;> z+{wVggfgA6CHPK_Q7U+t3EGscy{0*$60@X-ld0Oa$-6^;1CG6UoZwJALjj~!k!e+k zYA(Z<3M|4iQj$Rt+q`r2y|!nZgzVOJZ>{Js1AR(jR5#no7jk>R=J$XTC|46}>nh@~ z0gM;)XYg9&X2rI@mI9BY&?rkfo>(H@@CeQ!VJePY3m(IL7^)JH;>taQx0 zf#1ny_V@m!_rH5Ppf%tWfqyXp+Q0w+|88jbe?lZfD`Nwj|0h4SR+O<_!4^MEBWz|XElwtU2A)qsPO!gnZV2zD?YxA76Ni||YTM)L1t7S`LNK+- zddIoyh|wfg^~hOowKW}SbaeZug4d6Efdd-_PA>+yQ!sE&PPFg)rY8U->QdcMeSbF0 zK&V&DHl;2>b`&(C$rp?e&9X-dT2##taToNOIH;0!L#i9^Kze68d;XVJd+KtS<(y61 z6H!+9*psionZBFC~YdG6>*q%LL!@ifk0>WLX z#4g1;GDgv7)(d{*Zr$jIY*koF5;!k4d!-%U5=aX*YUVq9rn8@=KdTHmLyUJGH25T_ zC6{0FsH!S;0R;Ol!^@|u)F2;ba8e)k}kKm_uHjYYs%zPsAMBr zzPz=mCtgQ9gCRfM1EezOG3t|0ZqcuIKENYoGVFmv-AFp!1R+}}uz`M@2uXm+6fvBF zXnslAQ~_kt^K`I~?8nT_i$2tO^}I9Y^5*S(yXVu)lR(p>$M*zY4o>cLE<@hj7MX0z z`)X<;gBB9KqKqSLQ;RF3o>*qQiC1;@j*C7ZIuE%DB-7ZPaME4;3!o<8^<^0>$#vHD+gsj%%@U<88}(!d;MS?vkIuUoCEGPXRu%8slSY@%3DnB# zxN467KA-2r60lKjwmb3MrUCj_p{?*o*-!hde z=4tp|lbq8So+JAbYY|2s4GPh{nUW=>&*M8`aPdpTRyP4EOo$g!0aS%Iv!I=DB@hi% zpM-@Cc_)T$tRc_tQAzGq7U5|LaN`~IK`2Dgurf}|ei1NL7=$-Ux8ngf7d>NONKpuL ze*lRmL%PbSq|QmYj+jue0Uw-Qap)W5Gu{zG0sKT-i6<7^dIC+k)UQ_(nuW#Q{Z!UI zZPDWE$0F*$>w#YO10a@DjjnJJRFZ=OzesbouN1n$LI_o+-d$& z2JK%N@){c`(R2QORrf-}vs4{a~9P!iUb5}+_hv539Dq>v1&6F@Z%7D zbWq0m+vKOInaz;^^K${*=XWu~G!5cbgEgfGF;}@$GUYkg$!sH!*|E9LN%0;g5QyUf z$(TMaKoG}1SzrRnd~jZAwkphlXq_bN`+4BkPDWaQsY+2B)tL;T5`k8khWUWEiFUAr zbYq_W(@0okf%=~!Cqx7iF)e_S5Leb6_gyB$`+2!JR1`nfDWa2-JcA&B^|kwX>>}*9 znTg&3VIhA5_9+X^tLLC|^_K7$FOg5G4W&NbU2xAxVLD;lay%<$;dtJL zA+rt$z#6#?vL99H`)fOli^SoH0kwNciqigp%`=RBfx<>vBZg%)Z|c3}I0;wNMhXpL z5O1?60$>B83uLB@V@TBw2)B>prR{$YdVwmCV@iPmAmKke_LDU#yG@`HCN=0I(RM&p zOqiP!mdzM&R1f6IUdr)+W@{l$4C5!wUkoTA7Y#wpKp0b2mDGo4*byPt0iiaRFTz5+ zA`Ao#suMoaS+Znu^Y^ zAqsf!`_c#ekul~yzQ*601S_xp3QQ{lIH_nX(+3(@_l_7!huQV(ik z_nqD|jqY2KosCmgwVJZh+Rl1WTM3SO()&f)q1#Gs#N8|F)Xmg<#(u+!$9F@&vDJ$u z==C~<&20Qk{9TIoextO$J@y9oXK`U?ZCyj%#+m^Wo{rHA;0D#MzFwnGPbk5aglZ$( zq;bn97w}7Uw0wE9Q=FOV87&gxSqG)8wIW(}x+-5yEl&&M#DWDSYgb$M+vWcH7%N!E ziL<(N!GklKs^p>EiPRH_@RoXunzojvr#bSY^)e=d<=MhVbN8OP{t`B9Lxi0x*${EQ zoNZd`2dYcam*#VV$ha7rGS=h4s&cU=qmw*-tBHWdoXd_&a^u=!p);iW;jP*k$|jA= z5jFFgOA~ba?hVr8c`~=kImpB;Wo8b$TUM{Ic9Ek)d^c~>DY!?-b^-Q7=L7hImS&!^ zdY-03=F%zjW`Wdto@b*3?s^Vy0dF2}5%`1Y1Mxe}N77f8PiX$P6#V;tg++}Ifwod% z0sw4L0sz4Orwg=o_@As}FAaAmtmXFAx0q@y4Ynw5Ak$gm!@42X9AU}2#<)uwD8PVl z*dUb^6RmJ+NQs#NMIhqgD+AL!e)2dV^UnuCtq!(e#T`qjOic3IgLt3qxq;JEx3~?C{s9|Xeg>kfpW0Tlt zWs*!Ptn_aYghNG6WI9qxWFK-AxofG*W9j0lhjee)bwOk|X;R8lx7U}H$9oiZ6AW_A zg>>zbgm5y4b~l1{uEdHYEGp#Ufu55L&bev6lxbs!bt5HJnrH8DMcq;!-*x1ZT8ET&?`{G|sH%F+sE zbq}!FT0!-xB6B7;G;2a%KivZrXUvUF;9Lg0QgiFNW#&>0jKRCJ2 z*45i-lqJY>=B#-f>SZA5Vi1&fr^nFuOCSw&g~)$OfeB&b9M6lh^zSswk|@;Tc$~W? zR3;oN%#{wSZ)uGlz4Ta9Wl1E@-;LXdRLXnPHArD^ zucnhG(oYtYdt6kC2LNaH`nEcAyObnl-bNk5B}o!XlG1z)+-%ACHq%pIe+drd6i+_P&;@h8T($wo zJX|l#)RH^SEiD!()T=MO4~sI3`7zU&YI2!+s-`ND4V_+w+U45z5C^=~oxT;{#Fo9S;j80jNA^1Wh-70EVWR{z>F+X(ZS z>|Dbk{>7xHXTeQih-}|lGr(SUi}d`9wb*w4ATK1PpT6YA&Q2`U5fxOtO*&m>1Wapt zk41((4`nB8S($}+N;>S0$+xNmp2t-JQ>D+yO-}=xnhDZVvv+bjy&!&!fm<>N*p+q3 zCfghddQ}vakzcmKDc342+)y^D!Zi!plBZ z9poeu^-v*fn=XH9!GC-8fq7R4o;c6xy}oe!*l@*I5a!@5=N#VbtY^%(5t z_ZSosYG7K7`xO@GtE!~dD(}z1v20#0TZyq_GYW+&^RM|p_v1DjlMn)1#w&+v?}P+( zWG*nk>JQNB1?Oo9U6Rvg#SVM(6aZ^;U9`LSQ857EWg-d@Q}yPJNRY7h#s&RJ@lV2o zL6!vXcIyp0*j@y*!9G(vB2cf>#6!Z0)0rDOzNY-c84uHRF&9lC=AUX!R=Ff~`4#v{&<;sz}I_uB`*DjXo7-Um(_R=0*J7 z`2GU4v<4UC$0H~bT@b{H-XP;&qJW_T182*`5|*tLLKImDEW_n0O}O;QJZ(bGoxT3m zF50E-6ID2F`$7_4_K-TXJWr)5vwADuPUvICLQLC7F!;U-r_%BeCx=txU}~d);oBR*=s^H-4+Nen#jIeAl+Jr z8Jn@R5!Nx>$;M69gW8}Wm&NKBHbKM2xiE$RL(yLP)2QnEN6`WLhSqMVxM->v1rEz} zh&0-^Xo6GFESFx02+CH%tu^0>yvsQWegz^dUO#hek-V7Gos*f-8m7G~)gw}ns6bPe z&%tjjf8nI;OrS-fwG9O-&R$D<2RzslF!2h_yDD`(9=CpY$8Qvhvtjn7o_Evi(aF`F z&%wM-?1?b-8eEX+D+q($}58M+oQFk#7kb;TU)4gH&7^yt!GDzr%E9vNqQpY%yX{W@44o6e{w zDK0{pWW@T#Ds%Cxxwm85U9gBre*58d6w?Y6wT>@Fc`I;Ht^kKHZL5a$fZK;_^4!RC zFu^rDvuNe}meeW1?Xao9c(wK~hr14>g^&Od31hk?QD+QZmeBBV`a%b>YtVIv;G3{* z1;AGkAz(Z9tO4o2;=oL(SWD$QUNxFsleH%?{s4FaHJx6i8QOc-2ILJK7pqizA#mwA(7%8v4w@~ORG@%E|Gn@g<>ryw;U$@1TtL5Dk1&i1e@JWgU3eo+y@ zleL5M)tJ2|kvl{+Lj2bi@MV_M{GOq0J8*q7mYzej!bh~5g6yIj!!_c0*&kwKNzlLV zaw^@YuYo_E{IBU{A0wsZD0SG-;yEUXa}g8ZAC-R4%VQ}$%sOgjo&T#gzeYg(;i9Zn@nvV z=)cFD|2!=(V4SI=w^qz=P+*%~$8gZ)_K}lf0&YCoD7mo_QR+}zen{DOTwcb?QH+1u z!kg9SHXOB)ROT;DDUPKQZQO8Zg8i^u-7#xK#N_G{Lk;x zxBmt}jyiZ~2&*NUyU`;Z2F-z_W#4{l`)LdwYk;hoGX@`w9(Q)T7+3I)U>cyj$p{Z) zU}8#4c6h-TMH#rdwx$^gKoY>Dhi$=Cnlpp1pudE1Q%|mHrLX}bzo94Mw$;`x!!SVR z(K!hFFI4iMH=BwcG53U4I46xLDnd(MiEf;l1$VDUkC%-xf-2({L(A~M?*|D3Mp1Cj zc|*@R!<*S&qZxbIjp?q$N-&WSl}MLE+_sj3G1Zb1EJcb`iY4WF4lj1=D914_vt!8{ ztT2LLOS1Ds`a-zyh{DY3xs zQZ0g6Nd5tQ_8i)13+72xYa>Bq{M6bGU+x z5j!E)&M8YU$>j#``gnt6z`rrN7TKZduGI4|44Hv!n+fatsu|=rFOU;&#bouOt~!w` z5V&{k-T~K?M^djyd6RM zt z@J98oyksOhz3Vi$-(30c>5{vWQ{I%rfrR!)2|i@f@G2?;!n-v;W$G&OK&c)T6Vdj@ zQUgpID%`VYVDqI?dtnoZwgHM>+|rw2l73BCUQ+JdgpyhorA|kEpfDaw)VU8F!y|~{ zzZfZVqn68Zk{PnD@v`hnC52r-fbVaddJHw%jWs&L`Oo0ny$eksO5Y;Lg<_3yJ`42E z3K6=?ycw`O@fF^eo-3=BRy#>5?Gdpry)i9YcsFQPKXZ#8se_ zuytcA&;Zc9!ztY;Z_E+DVvsm=y5(H|i!=bKn#2nYv1Iw-1{7Agg#>tV#HJJ0<@0ZZ z*g@a_9=}kV-|Ny!tDrJ6z;aG4jf0?zv$C35Yj!_3AB#wU+P|P>PGh~(_>6MZ_wYsQ zSDaRv6}*yDes#EidDvQAbKLt;Z5}$;4JShAIes`b_|k!>LpNI~&D;g5bmln_X*($X z*m4VZ7{pr98;USUD4Y4D+eaFnRY>gxuyF;xP8}`J@Y+14`+JCM)g_1rz|ga%%%~d> z^~`gCVT5fcD27G*bvj5uBkS zfsoqz`AyG(m)6Ws^rfiuvg@h%?&RN=nNP|e-?Ffmc8Xz#M*YjHjH&!_!=6x%_eB1P z>ln2U4Y=>Df!Ev(6@23f|1%Q$bv6I?+O3oJN_V8-tbY%S$+!Pt$li{3^Qo4kQdLH{ zM>aRc#%LSI4tW}QA~4srOyl!3jQOy29742H0`{cZ=4Wc6b+rd1L71&QdDqyrtZ;?D#37{1w&qjWi6kg$vlK=Nzfmm{;6Z-oOnT zlF2zlfyQ3&FkOb$;=x;LCOm+<_vx(sm7v}e=mMIe+`M~@)>|#ceAh-H)B^OvwX^le zEAaWRGK<{g4tvvsUv&6Cag9Cq?6Gz^P}o{g&*i|J?RsUm>vgktMTfbH20sz_?S8RF zz|I~(raoUupB$op;T%C-%07O2w7w8`i{pPx?g4kPBVb&^_+U0I>IoUQXn8SOSbjqD zSb{`VKc!We!WoolQ1%59-IEW1=a~1!4}GH|cH^OZ2lOEYR4tYA+xME&s=CF#*bN`c?d;w#Q4!NqE@AvzSRW^U1)B9`NU@O1B96^%P2i+^V zvIlO-@9UP{ez^>u2r57Dcc#?gFf{vbmDU%I4+{fq4g=d>{fD%)Rmps8Gz2T5s@@RW z7CM9C^}!Uq$8!3Q(HyyAvGn0Qp>J$NFGC`%nk3N^A1-Ys^+D?V zZ|K?Z;N6)UGdK2Z*99&QAUEvO&+k9x3h?`DtOBd7yo}bvwAZ(SBFK0~lBQXwqOUJ3 z%mfN@N*Vx#A;QAI;o#$kE1g09mKZC%@Fq2_SU&d8+azw^E`04pdSV~OwK4x_zN)vL38qu(7wlo(tH48z?hdvV$d;7mRmFh{*@`Q zsd20ZpI7YUmmxjZeF*taf{{J?*a20RqDkq{K?R+e@$EsxSNllHdXGZU2=|@WQx126f7u;6a zlScndWT_mv(nci)=WpbD2`xQ&Szems<1eJnhs^ z$j)U&T=cW?^Jb~M*5MIs-*^*@zSX-e^^ z`R$XGj2@=LptH3qu0vHCDlEtQgMaN$X%#FjvF9=EFORA(6hDzW8dlPF9EY!OMnG=) zd*ARcQ*LY2HZxyt=6QPRmgjAIR`~4sn`g5vIP+>RwH52|yM1<+W>?!{!HTT3F^nu~ za1~;us96rHb3uaJdd$^#=AJuB#v31I`*I>~ltyLKXn*qFIWAuzW3I2Aqvx^y4_D_9 zELhNH+q$-G+qP}nHtX89ZQHhO+qUid>VMsX?vBX8$%q`~8D)HXuLbom8DJ&V%HcgI z!xRO?lR9$yb@3HC;eXdtwMh1JcSDDVw05KO*Zv{%as;v$War|hGi}9*Vr=wvpv7+w zj#J5VXX=B2otZ{tyXVr{#!F&o?;)tAr!L+9QAaK1W`|j|Yh!=6w(Sbjndeo0cvfks z^ySw!Q@Q0+GTYL6O6W58(lW6nLlX`bbZ@J>l~o-HuB-XJP;ZlyaLcAywcL0p!pc_Z zG-*>qi;f*mmOix{yT<%$!K82EB8YCMLVl+^V+UCbx@b zSWfFP#3QAH?B40`2lKV;<4LVu8)c=ICfCf6*u_)EUj>%PX0}S#Z^s@=IwrA--ZszJ z%l@5}4fAiaXt^8vZl;g!oWyjn=GJdHx7I11pO+#cll0u=5{psq%+lMRJoD>X0xhFN zO;Mk>8-(klxY3{^L7>{$_bS~b=xAlFZP(@EF4We9?nDEqU3%mptGwP;(yX@(Hr z_;2t*(SuNQUDBP2I{&BJf9%A{@n75!m7?A8JH&e&Xe#m9-om7(uUwafUR9y3zz0f# zZ_oqCh;w4D-%%WBqM&`GNubbfn(Lwq^ z_S|%Y0sGyZ2{y}zM1{KM*OC%TR%cI3uNwytCf7P%363kH7nrH zEXwnOkDOwE1F44xL}9Ak#7!X&xtdo>uN?#0rj2$16J${-R;_#)Ggnk(<4xT?K$ZMO z1LjTYfYKs1t#%QE7I0nDN`hUBq$oi4YSD4y7ReE9xR)7oWYH0`u<=Ld^MeB6$UUUu zm?%Z7B?V4rYh@>=C!F}#h9Ktbw1gx}1kvL(9HbH{OCjqfPCN2a!f{hG2@T-b^L?iF z;Y9=5I7b!h5f5h*Ph+Y^0xo*bv*-!kvnCAUNIwJ&mlo=Qo4AM~@Xl#?gT$xxmfrLk zdGo{p5-r}!5cclu)#SLT2UAZK+An8{H;%jaf;*rfdhpQlJpXfgEZkr>@sQ#;(DOJ5 zPH{jW^nPG>eSfby0fr<$C{+Ot<$lsNXcl=mE`74A5K{Ay?mN6SIIuhz*g*w$SiHeg zd(fEy_qIQ&`&wrpY+HT;Z4kZ%ew4u2!A5sJUVrcg#@P{s955>w5RL3fX9m{a$Vc|M zr3XOB4H-)g480(k7+`YyOC2yU=fXJBBHz{hRGSPKQVt^90M!mGtA?7}Kr8Dot?FU1 z>Tp{RVYdQh-e7woe@DFA@U-smaYtOZ1H|fq3L4VwRjIWltkub_uIlozsEd+vJUI*-|SpEW6+u7SRgIJg8Uoj4#w z;;1P^(A}6&9&x6(bb<_P=lP8_KLzGy?kk}t9jJ%rD5{iFx2msdJ|F*(R;FhD@(OAN zd6K1t=F%oubhml)(&hKe6tVR^X|LTW_s_px*{*BPWA?kuW6E_gbfSAvzfNzvZZs?t zw52I}3DPKXFWm`DS&E+}RN0O`d5R^h2$6Tb&q8h4jy>}p6jZ(NP$i|qGS|P(e$Nx%FD z|NXq|4%{`QAt&YoEY+&OdHJE6$I)k5d`x9~YRqM$5DJ%}QrI|Q;Pi2+xJ7uNy$`P+TyhoVPQxM*Dx#em3?t26h_|Zyy)_Be{=RKKSN*k3)d%+!4OXeTT|`Z^?tVtSxGNq zY(<;Fi8{UT;&drYsWbi#1BPNggqPtH4Av>xa|%Q`{v(?YJ8ltQX3bb~~+rQ^tnK*TY!sz@S* zxp!!yJb}2gsv(ZHP+O+uu`7w+N`X)c_clv@t-5Qh?Jo`O`XCgT-=0K~O|w)1cklwb z|1}8fM0%uR5g4(Ha3UL_i}Yo4kvD#Q=m{V!yI2(=nW~;3OKRMjAG#IL0RoHp)WqK| zm$4b~1!whpj}-3v>3gLshKX~8c$X>K2}VTA+4@nFSmLpBaV}V)FZ5edLP1+av#++i zOF&Wo?4;_mKXjIf$SzTKs5-c@R}!wB)HSB{TZi!6w65(U_9aR~s((uFv_r;p=R{fY zUou?4qjFbh)fceSxg+0|b5aq6?Wq=O*57w5XTmMpn76?IyzEgNEdq!#(MTrfJUxPe zxQVzL;#p;YjxYo;PZ;ee9SnTridvoR8Ri~>Ryp?8gE37k5GqRp78*M^0Hv;0cT zgB&tkV5SE0i2UN&bq3G%_YCRjjcSH2u}9;mk1W{(7u;fi7?HXvPa68Su8jXS22-pv zQH;^@e6df`;$|i~V%P5-g79>)YvE-GQ&S!>RwLEb)}3PJz;QM6zi1;F+b4O1p-pwP z&Dk?F#!dd?u0}!!pKn4&nxQyC2UBOLpQF;_J<@joN1_pCQ)HN_G@fYw*T+ZM3>I~~2 zAw~D!y8g2P$SH`&?J`jYNXi%$o(ziIQW=NKLL18&2Ub2ygL9fv??%DT1QJ$=gkNde z0BlB3s5aTI-t`YLZ$g`0JT|k(?9COK57H^#h=n#7e|apjqvIdfC^$$`hquk22s2LL z4|2XlYAH)0T}6H57+PY?Z@e=eq`lhyA%lNGz)^rQ zZm)@RF}q#%;=;>MEx?x4`;J{ol(t>y59&IKlVF=_pz|<1y9US}=AMq&bbdDF*^*MM zins;%UzVp#XFjVc=v7g(s4?Ztc!hW*eHGJML06aP^-KN_LsPo2EP*@0*nLOT(+dVI zE4M2~?th)~X4>`hjo(>|XC&FcN!ewmjv4!ZOOqwb#bqvd6^nP#NR-X|o9wDK7&kIi z2?b6Ue@y1A58OCbT4tZ5%qU>=!aoe1A_J^lNXaQUO^0W%kr8QqBz=?Lw zo=Cbnq&&GC8mOfhy*J`9cgi^f}#1=E0sx)0kmZ{S?i^|9W;*73DM0OAh zMu>PZP7$qEPt}&Hx{zHg4rkeKLs%*#nYI(yqq>f|jR*jHe!)Orr0`7f%@nc~eI#{Z zOYDO{4^qiSeP}&TTlCZde6n90Q*bPWW^YK-zF?t?UcrNpNUm?6veZfo^SPr9<>wU0 zyg8&5pa(U>%Jd#{2Npq7pBoj^UlUaFy3RY2xF%#IKP08B0-`2kSjA(%v49x?x;$lC z6lSkn#ENALfNCkQ`SRq!=~NqjUY3c1WedcLI;HJ7$%zNX_L^O7^gxqHzP;|MG>U(D z{?OK~6L*QcwQ3)yZyUVv8T8e0Z--;DdvedVU`r;c(;&;r*dee#8*IxJV=KC~J(EA$ zv6H0>t1VN%Wu1BrmQ6(qKw=)cV%r7c6xk8Cj2Tpq7y1R=%U*4DI5UgPqYxMuO*=ME5lWRvo~9{G!@@aU=N!z9m%>ZF^v0VZWzWq3Q7T|O|;!WrrVf>N{zl;4CYprW=sHN^O(ld>&4$(v1^wXYec zt)sFnhpS!Eukr}CM!2KI^B&L}M!$mKOjor{gCM{?gFEjQv^c;@@kkZgLwag5`_?7=Y(9*B7 zh`k`fjAK$st5rZ1bDC6T5@Ii>+~%7v{&S9PQ&~-RfQ|UxZOTm%i!}`$b904wcQe6o zEyY+)43RX`aLeeV$eKeXa-W zwcGb&Z#|*6_x+iQ?eWHD?A-I*pxR^i_7PT=JH@w-%HzlIb&{@~_cOU$E*7`_=k|Ku z^|afuhsWu7NN)j-?kljM`!Mry;~*aI>waPR=hW86Lg&3baO!nEr+;V5mDS1n#Qf+x z^L7LbQn%}FDKh-A7VqbR=-fzcl-Jto8v5tpsy623Gn}o9uV6o8oII zJezFQ=KG0BZJE=W}oOTD+3c=gipXR{3c|?nkbeXzZ5T zlN!sez4vwGCykv>S93$r)l^7%BOfK_+k7i-?>ti)8u+`~cj0}*^6Snt94Txrwl+h% z$JGzk?5$6J1?8>d`u*QppZ!!YJ-v_5+h2Rz-&w>?&Xdj#tQlQ%-k;!|LM1hvQcO1A zclMe#54)e**D*QvzK0jC&Yf=R-S0kS^YcNwucPLMP@gct&-v%+*%gCkvcj4oWsAtv zSM7A&J4bhi*QfJHF|;M#-&(t??ys5Z7a%^4E}8P1TJ6N-w=b&d@WCMiYv2_~LP9)>P^ns_YrnJMSb>oW#JLhQA&K=|x z?#X6SADA)CMjeY6i%xXo=J3+yV#4#O({ywM;#KU@X50Gp5+9Qc8%@5`bhG5SL(`V( z=L*b1uh(D-$3L)mn9*WSvO#7OKWdUBvpv1#?lar!WfSk)d-_IFMR25iEBC4qtzu$J z^2~^(KrQ%#=Xh^j_d|DebT1Z=bs|WX^vGC-RS|s1`9#*{VfCSe{`Q3ba#owU+o14M zAc0YZ`HjWRpVkKREd(4MGXyN$%D2gnx6~JC9+?h9#-F)o)`fod&h~P2IK(ZGY$O!~&V!EQAS*jbX|OS%gWS zi~N}p;T#g9|Di((vWp_Ei8dv;+9RLhY!67fiTMMB34n-&LC5LWWo}3r5Xm6^L$~l8 z^LfyC;9wNJA<-k$BYjQuB?=uAIwF7P#2|}}GdfaW6u>3gB^n)vc#wG@=8*8={}0MS z>}d1|gHggqawa}EK6PyJ2*fd7o3NKObzJ2E(><|Eq$cr~^fuvj4CVpgo#`Fwo$5Vo zix4l-7nvT!8i2A6H4ad$U;6KEXD_aq{}Q@Sj$|NNn+`=60aW0U{J>N#mx zIVmmY%$`e(lR6z*$hi?=iwiTW!V;5PD6qnuEAUq#qX5_}659~jASNcI2J8V{W=3*z z^UhZu03qYOvi!x{x_IWzKF(lyH0XIZ(6-{80$cS;uYLTy^-d^}cBpoo+4=l^yZ$|F zeGT7swbQ$Ptkzsl-f&wTv2<8Ewd&b9eZ+xC`3rRJsK=^S0W5W_U9LTK^iNxG*kj$Z zRgl6oJfQDG&Yo2H0uzl4wm{^Am?m);ZwNZr??tegH4PI5Gp&qCG)#cwI~{l)FKKIP zV%J0L%^Z6~wJfP$r#@56G$=FN-6%OMx%|jUq=qYj7UGna&7nGWqR*UaGn2fStc|aP zO>-=!PoaiG^?VHZ`L+C@UQ@1~4qySDSun5vj|;QDNxQ8aIyw9eE#+92I#SU%BDwz0 z)-&qkHajzMBI>v^BppDqYfABEwEQJr%)uj1tu`&`z3Sau2TXRVNeU=)yci*swUJ8_wkDuyCnjsU)Esz5;X`wW@N3nym$_O1FC=T)jGP%B?iGwM7`X6=L zP?0vpOmVK*GB0a0m5NC6h^&!s(vr%5-m5r)4w|L(wfK=}b*bd@0R)0Ed%SGQhzoqg zyha|TqHOLds%tBePY<0Mlad;EB2vb^sg|3)G|SVN8qO{GK%FB|NH-*o2`J)Rv0RQ* z1}^HTMqz`QEIBa8Z^(}fgp!}`lQOu(QI;+i1u*!)ydwj{Hs$x@wR93Bg-d?Iq@ulw zH>(D{Y2T}UE)+)+L00sFXmGR5NvJH)C>e92#LEE`65dhslV$zjXF!}G`+4Ol-%F+T zis}p~bV6oO$b*H4*(fiI);Pq(a(pz(E8YiAvJh*0Vael>DpLf&|Ah z*VvD<=KVUc$tCzH34{cv>RyLq6xrfS~rs8}n+#Oa*Z zH`O*s(Pxa4S1D_0JI61YM2}KfDP$e9VBAL^s%~Ydhr}7)A`OWaos5Wj}(;sd}_~X$jT3x#?#;gl>&`9R6gcYVEcXdwwt@)z*XFlIM-*R?i3=XzI<6VC zDgg}p5?K3lha7{Vd;F?pAZGFKP02tB@VoyTqVs*+#D>3qZ6S?elb-wnsAO_#Jg+e> ziqme9>kiC11mm>21RQe#+6tw_cZ?r;v>u8T%mG=JZ|Q{buuRU#hQ3Z?6+LPuu#m(a z+46oFXWGubiJv0_NbGMB(G-m^<14{mz0mZ@f@tQWj%h#YL=ug;fV`8iZ~7FR9=JUe7UK|_ zV#EMLt>Je^n1Gx6V9kJ~T4Y|EIORpNx=7svo^vVGejsZDIORW_RKw*TLTLC^R%26< z!1K%G^J4mLCq$(zItV4%s*K#^q{NI&k`R;90duZWU??{F%dGtR;yMwD25lE2uNuEu z6NjFyB7RP>!GEtwF`iL#H5a5tT7uTeaZ2kDofX*Tn0oB z3O$l=8-&=1-6dhcB8t!Zn!VGZh+CpdHYm=So2I4X*Y1Suxx38$n=CGg_OuF8ErRbebG3*waC4fI(4dlrthyBOmHPn^<}g)rOeYPn*4{M22duH@*_^S{NOE`0{U%?i z(bXt3a{CypdZqPR^5z%Us;SNiG1c|XthdD3vI`RYr9wvG?6=HFT})g4e3ip>ode){ zYk%&Z|Ll+wljOs9*QdhwPJl6pfoZ@S^!3_c0W+_($Lw0#G8;)`*2ZFd^K84+BK^hw zcQNu6Y!;N-mq=bh*4|NeX=KzsnbUTwYc@IzdUk44V{2YQwexxxEk?4ni3H8+GS@D& zAD)(uIt-PB0}lCO!i-DXKrt-@)XtQQGOdBw9f;ca;{`jK4Eu1AZVfEFwrftR^)NrU zK6D$>XHM!YgKz=O`NQxDAi{m4(n9c`7ZiL0`*o`1Hp)C?dqIkg^CmjrkDSu|28ghU z9c(ZI0Zvt& zE>srFix~i!y$TLx?NjGS_oJk=499z@H;(Q2oSxv;%st-SBf{s$#-SX z5|sUxwwBgiD`swjo$P4PyOE=G9p`|+Ud^j~D#(_~XKu=6oZOH$mt|Q!u`YuRHpXF7 zZTe+bUd^v`GjH>JHUz$%@IKA~qPIgi9)dZ5zkPsxOGj?x?r_FozxI2U7L$eVNu#9$ zZUq*vAztN2Mc#2G6ch4O&I?xQ7{``Y{H6ul{EM7=CHADrh?$!IdIWYF*t<^e-B5rs z`OHHFy6PCvAEhnq0X0VF0Pli)>#%(5C_N>DU$K^3$2VAh>re`BcEMk(4t92J-6{-L z@7xKbZb$cR!}fV19}2G1`W|981(r#R2t*L%G+;ov-W8KSaBIiAJ0*G3y>cwN&tV|E#juRz zB4Fa1z#EB|Jj3g=_jj1_TUM8h9abdx5;bR!A?=+eS z$0d^nL?TD_uRF4`VcZR$3=efUg5rr$(vus$)ICYBMmRq1FaHP4PJL6RdzF4}K>Oz% z!6=WJRGwuUb+Czl!6o$k<}GjDzF%!3-y{m-I(9`y$T#aZ@fO#id(cL%4EMsDS06X| zW)6?=BA%EmN!bgHFCL7x_C{}3@71pdxy#rw&0bV@cj-;u`y}YmiwHeu20$SZ5b2hm z7JmPsk2et?I-5dH4?ByFJR;2xWCc~0z{{h?!KbXqtEj}RL>DBnNQ~KPy78L~woivp zw~i0nqltW&ijeW3iiyBRun*t^g^p!GT`^KtN012oh*=4n1a%L_hiH*8Z z`{v?D28;x|lMLqp_j=+6rdleD6->{GCE$JHu38dR*~D|u0Kn+Q!|>(qk4?Lj@d={E z5wr1;HWITcl~2Nh0CPQeeIN^fyE6;u zd~>sB^HnvHBYs=lJ3f@C6rEt@+u2;7j4h;ky(0^RdnjdoF^;6^r86Qx4x5Z%gr`%& z4+E0~A8c;^6csOx)eF_3LZfaGuBJo5wcrl$2jMN99oX|LoJ>$DGZ)64PhPb;4tVn5Cx{i_4_qXOJzLTQvmCoG zCUTXF=P9zr&Om)9fO}_By62(1@0K5YO&eO(8$1(P4tLxd*mpMO%rm~B z=Ky()mUI4SbNcX_KA*4TBAH+>$-?a%I7d?Z>oN8B<4UC4Gu;g~EsQcpjQ{K%ut8uJO@Zaf3&L4$Hg!JtiF?1n zjGs`$`q~V_2eQen;+~X}AFuQbln9s>oZP1)<{$>%b-m5-YEhnJdu*+=e~s4DMU_cw z2Xp?sLMWDW@`50%D~bKMyS{TY}3f-B35M?fzK zs&{9+$Sx_WHx__aePwBo)vXh*|&9v4_~=;cGV{5`ML)c?9n3Vo{igiKHahfLN8->s?x+{c}J>JPU_#S zy0U&n33o9Ki1kIum!~FY-;K*j*OH6*TXwUgopg)m88?qx+2u|I53}T*^?zFr4?frR zO7cvBuAWmyHB~O#FwSjd_+&Wdm!3&oA_(gXAms1Qvaszh?uvu|EXsqfk(t&t)Q<^h zeEFOx~cFT#vBLn9Q54C~5a9Z_`J)a0p%z zd6#=B@Uc_yDKzl8RLoA`RJO%hvD`xwlx{D7v`pQep-D@R_X!ie((nEiI_n>l^$*Av z+`ZnQXubZGT7y?M%ukcZSumUrj9Z?g#4~nLYXr7To1c0)qh3WoD}uWoGW$tne?2OH zErL6LvO3#{KD#7-K)_QT{}>9u-=hXNe`mS{K+r7wDkQ{9JO5|6J`W7_-?IAM7W{5f zkS?Wf^w;<BR%pi?B6cXFpkc zzk)|!H1Usm)<0^|-zEuJy&xQa^?JpJ=MZ2w)o;UT)jzdao=*AOkVStt##R-6aFmh0 z@bFJ=F~i@v2v2U0o?qThsCDMKc(|-q;eqSwyz=u;MucYLz@EqPc zDUWKC?$mPbT;Z?c10)bX`2hYq00^Ouw2CQ%9@HN1&o6H;#9a1OfT+P7G5jZB_%xtX z9mHQaOx%w+54>&Z0;pNj0%g%MxO8Eq2k-{)CSpakx@v`HyF=u1#Jk5#B#RT>U8d_!wiTP0#V3t1i7C!?tFQKSf&r!ZOh z8CziAm?U!ax=k>O5&RT#<6zKLc21f47%Bz2#4L!XOIuvGl|IvbvbxuCPFCk^TKKRo zv{6)HiJ=SdJSGz)ga%e*yQg8+dYVDi5F&x+Zgl75-rstE=OI|mD|ti06UgAJThJ8% zv8NZo7yo}lvni7o-KZsJKsDOzFoZoC6#5;-`dB2jnJ{BWh{C&ga_wjoMJtd)eSNw8 z5Ns1|ki&j^6jsQ1n%6r-HA-VV$|?*gtE*@#2u1d|36J%^aK?fyPj;1zY&Wio>g|S> zBssGb+~`u+HGNK)M8uX7z&_E)OBHE6ZP1EN!^lN0tPiOVOT!4}45@K=uR6>;~0YoH=#fF&@FHQ_CbVbxf_mZkSXT+zOE zrF=7p(^jEXSZ^9wo*NeM4KlXc|CE~6`k#A2R`vra5ZX*k2t<9E2;yBBp5<&rf1gL} zB9T_95ypom0lmDV(Dl?p?vojh{@x8H=Z@Sy{~`Dsi1W8Wte2_GvihV?cG33W@}<1S zD@pL5v^q{*JsIi77io5dBMF=-8oUE(ksY`?2Ce~8Tf6TOtX~ShjC1PUEIn! zqL1d%hdw;U5#T5v55EvIipu};&N3e{Y`bHw;r3;lkeEIsCu<*p@_;RP4|@^uB2|h; z3t>C%LHFV2O&;Yd*}^lzd?xstJ$va2P@9Th0e4~{ZZqlT9ej5IefVDH#vM?q(SONy zg7?*}JDDkj9ZhCbMdGl#e1rpEG-rk_(=3vdc*q)(v?G?YD?c&*x21FobTG!zD#|}8 z@|=a>`Bv%>d@#Wg$>SzOFDqn;DSL(~eCIP^g^+&Ry#_mXWzG$Xr#JTUtRv{dd9<|h zc`dQsPYp};Rwn0H!s(p-62$YD<(1#tXN4kJre@f$LV->uMsIADYn06xA~IwI+4BWa ztSBeSFL38Cv#ZRk>aBUoKqd3vkt+wOg~sL_5#0m-5?z|>!S*+=RXuI8AHz({G@E@p zbUY2NvZKrDa_CPX(9{(p<0O<<7Lt=abZ3 z6;^4>HM^8_?QY{NgPE*>$l(?^!0DZt3GY_aqJ(m!V+n2GBq7rPa(&*4_0$(9iD%gjH4WfLe|dUt6tuM zg~v_3Kt#r$ui6pY!}x2M8lQZ--66h?weqCoqjtN->8_>pmeKRFXy35H>2V(rHN5Zr zdY}4o$DC{VP$D8+5M3q-NuAysY3spxB7~23hG2?LHkM^Y7*?WhdKg~tOkPd4#auT0LUa`y!a z^0qqPO^(qDyhm2|3@2LW_x^GvQ%f>-n~eKMoY9uAyG=Z2+Djzs`7JWGZ;Y>!O-?@Z zPqkXcRNf93{!@fGcrW$^lCDM?s`=(JMw}s7e0knqFB+@Id zY#p?{GY6YS-~NKnVtTvP@0rK+%dO;vL7>~&8mIG(_GJPJo9*V+eq#n-7jx_;a!vj`q&t<8wV&~+Gz*a9$cJyPbHItuX6J# zk={zO801>TD6h1A7S7KZxlGy9aH*FYhVs^K3zqdS3&+C+s#7^>>x=J^Mw7P(IYut0 z^TkUa8}A~F!67`Z#AW9r$?G^*)B4hq9JsX?&Lu)$tNXJ$zLqMS}Zdo(McRp_6o z>eso2MzBW!VxjThkiMhapCG@xapLZmd_OP{No@L6C0(dSD96j_ z_epAoXXn0${d_+p^+~&fbMn(HQRJ9(Lt#btfKm3CeE#_*+@T}mzSs04juo>7S>~{Y z~6#56w88^YA z)a);2`U-RVk4!yX?Rse%oITFNka)f@>9d`--3;@YqN8Wa^O!ggY2+YTl9kabpqB>) z1%gcw!up6frD_$;@qk7uWtu{Sg3d*K%GD5B7^TV-Amp-z$~01J`kGcC;6m|CT(a!PhZDWC>vi9+lTRV|!AiRGAzs)|TBaok9)h@5Ml^g0HZW>Uw zS%xc{IJK3F{2Z`|$PkRWIVDjH5F=i*8{iA${0R`FZa!q3<{dWZRf_&(onGGA%y4#7 z>K<1uhHzM^V7Li6Z+fl&x609wrydYzS%wd0Q|HZ8WphOXVAJWg7QOMB%(v zEK4?8UBgM7g=Pu@g(#jm$OZf3dYQv@zvG!(+>+1`jN%GX@I2d*dlx24u2s}jn?i}hYh4i zEDR@2l0`PN@FsXJut$#?3p0tZUzX>B;bwy>kiRF)RZ&dgsL#x%)JSLqt$A2OhzY2Q zlB_-X@G)pZVFec)g-h5V6i^7IblNfV5lKSmU@D0!#IKqrmQMWBBT z>uK3xh&|gN#Yt1vEtC;t%gm~1k-ZI+2-jE5_>ym~ex~IZpx4vt`>e37&5c8Q3PabI zWNpQR4}TE)!hPWN`Q%QC<`alWb>2Irl$S*2RonCAReK(MuC=cegx*e?dB;Bf>qCIG zj1otC;g%(Z^6}JT`!E@S66iDzkj=*bk&YQONc64!;Vp2!K;_UB(0l?M(|8o#7Pmu< z`iK@r8Gw%*6eN$3oUpiaw+jEPs6=&(B&kK`4rGwxh2(%kK(vGEt!7zc-Tv8Bwjap& z6TQ%@y&->+SZfb&EZ7_2_Ilo`+q;X0Z#o-Mf;MUQ*?IzYhr zOY;;lSNTR>cdjHr{caykB9X*y!JFAWekxdpe2mfZ42(R#9}ph^Q6_LE^EdZB?>K)p zL}2VoDS&r3^kPTicCY5K=(Uc^Uv$&9`P#*E)8e{mBU4MQV50PMRQ7hhdlacGI$&zU zu%&geepE!pjQXGc93rk-YcoKhL4QOx71ZsBPkX)lJi~Tdhh{>mko$fOn0Jd^!eLW1 zqn@cHo^(g_9$rstc=rIcr_j@JK24nj+R< zbzt4nFlpL-PpYEa64v;Izg|&YmpPh1{%C=8Z+v^*jbb%*TIo4@`j`!!GL`RS(MNsE1cYH#l{T4K?3czYlk@;I*sjHAf)UC?jmp`BouI%!mjl;E97b;ZdEbuoFK-=j==i#mIn6WF(y*|woU-PE{J zr7Fn;9Jqm0P(K9)5qDfm=$xBQ9VliQsC}TW_-F!%R<6Z`J)16b;X znm8ZWk+1QaLRokRko&oyT%iqWoWZQ^i{^J%gMA-cQ5BV1$?@oC^0PElRzU9Da5Sh& zvv5dVNX}A0N0$wxn3POWA)rGs3{EaA_L^S1p@3Ir&xD64qcd9w4$vSx{B~c=N;?oF zXzUJC!qw6gCklvEbnd4zBdt`YjfQw5f z%t=&czxbqNu#}D@Vyq*n)dk{WRtU&7oCyTz6>qm-SLu_do8c`lfa zZHvt)w%GZ1#ciB{fuAHOOeT{slrYt(yGkmrMVK5%txaC6lR_;6qrE^pRU|m5$u`6 zGzKQUxFg?*RR_}|g8f@{sE`RU%Y!VOR@_O%Qj#Xg@^>{|gqQSVivcbhXevt8}IX-ra9KapRC$*2<<|$AGZvj@hlE9}SzBPJzqrS4q zCy`+na7wCmNx-Oh_UJ&7b)%`K0S&QPA7v^!@TsN{>2atE-VTJ6MT(Y}K>4g`egHRrdVa?`p~!P$cGVV*6p9YYhkdlm5hENbR7_?dUemZ>$sgLLnQ6pBWi&^Wxh zB3fVwBhHfsfcG5uz6q+XDcC??77S5>sLY({-!B7PHpBiC^b`NPqIuG=Q5X)^1+$?) zPw|bMY!Xw}lqRi$`fi}4Qeci0>?bRPErbV>j zY1&BOxdnR9)}K}l3)YQRKs}F2{pon}bez3-&A;tc)XcUJgsnt@#v)kQF#~dgy~p;s zmW}p(ZOYO6&H%~f*Slm82;+sURytHtMUb(LeM5)b?Du}4Fbq0Xl6Wi?vh$RY`%YT? zX4A6L8+$*d4dtK$T7ZqHzynporcemYL}f*$z0;@=Y{{_e;0N9e>H(Z~DQXj4?j`xu z-_;Fql?x;+S5lOdUyhNON$4ti)e~?Ph=He(tK*3Mx^PBMbwdF#mTU7B3hX$UDeSBq z!w2RTp@jrOD`i>9wCi*HmwqSb75Sv!j`Zn8q8c*>W@#^L`cR&lIh zJ5EZArI&Zu$*9Y|E3wcRtkEY<)R0=5k7cM@-Eq*Hq9(;3HrlK}e0o5KPg~GK6J4-1 z#UWS=t*=)>`OIAGSJRg+Tgg(TFGBv!c-J-|>6JD7+h6Q2#dMHChq<~WtPS-#%EGfa zqg6Oj;evakX1nm-1${NuH$|RjMpVRh|BjgHO4M7@0Wak8I#`H0#1mpaRnDlhIM_S| z8D-CXnCR?Ny%mMOvp3xSB3i?-bo?hB8GjNaZ0H0_pw#bMfrwNId(R2|g(TyCu@?Of z08$eN=BII&;(9ZU@}xBw{aA@dp)1QY-wG zvN&V9W7UK>Bzrw@Z`3)Q#vJ^z43d(AG6;n~gH2HwPa@>Y-jD>U(JHPbm{kiToR}Y# zK*zj)J}$p~y#wNtL|g?kaXa;##|uKS)tJ)RC&0DPk*UCuroVMuHna@`^3;gttVW@= zZr$<%t_ufd;GRE_bAa`WQ!nokcEcE_EFjpcz9C7Eu70>+`>Ep-=sP61tc8Na!LP#m zBBL%vQL((G=mh;@ga*K9fQK8ZR07x%N3gR>V09dwtZ8OO3+PjG(;bZ?#gJ(Z2}2e>#WBNhLMLjW zz%lc0-Mh&Ji&Z()9IRY@dPB&rhGui`4kTC;E^Z(Y!>wreyl6Pbr|PKbc~aWWj8TT; zr;Jzo5GO*$paJu|vwS$V;HXU+jrl2CTgr3}XU>X}WLf3fmCIR%WnC52jy!e&eO6A` zNi}jTmY>M&q=+`Fyf%CkmQ4$S!+MZ~YxB`pNCSuXc0OqE`l*{lxMWe%PZD1`ytl0s z@=1Z8T0XR=bqfT!mVd)QpyMd=gelzwE8IjjZc;MqCD?>Zk(ElIt^A=t-MoLK9a|9SH|EBarws|eS}peWOAs!pHcHSD2pS#6>UG|*-DY$rV{2d6`Fi6J+-7%)rb z)-`LcI#d6QsC$;C(-TB*7|{t+R|6Zcl@&)k*1RmkNBvRQ4Zj;B1lUrD+#%cM^=Gpr!)(^lncD2nRfj7wn=NC)=BY&M5W{l6Yx*44BzDTtOwIPEA}FiO2};M)IcN{ zX>IM9yTMlBsMq_O%_^9?yo+=0z5Taw=&*ozu)rv@^sqtK5G0#aD`3u1Y<(E)U%x?>m(BG?uxu=a_@#@`{g(DMCYRL?&7=x0^V+B<$`Jg7FL#$jJ_U zO&AJS@QcgMflk-+}grwDmZ1LIfRHJ@b#anbG*lg<*#?oXMxeton@#@{W1Xtb35cF!*4LoK86e zi;PbG`xa?z#P!TN245^ol{Nkeo(}lJ`0U1EQZk?oMWj}re(7TGX`+$VpZZgZ;V_-b z;8F7PoKOZW(uReso59I|s*c-Vx)?5bR|aFW*!h7uTyWi|XMJ~*H3ObTUP%!isnZYq zb#Vb+aINw>Db{%%|NJ?^+AE)%?IVoLuv_7i5%H8Jy724kZEde_w|)14#P$51K!|+$ zUmxElNjBt+PSb(UjegrqC0wPL)G@2h8;8m7wxs>njRxWGpH0y;Qr&q zJ-ZL8<;#(0x)(-Y);M=fu|F;|hSuhIIh=5y&Y7Rpx9BDEjLNFX(r^z^+j|10qwsXs zFeDKtP(~qBK83G<0$&E(<2x1Ck^g^Ool}q?VV7;owr$(CtIM{!Y}>YN+qP}nw(Z;h zor##3dCQ2*$Bf96`#XEBo`7BWnm~|r%^zR;LX{o4E!5RsdlrId4-WQ{ zRVxjmtHQM%p@v_CimwniSpse#5TBjzg{g>t)d*3}Q!Yf^SVr&(gMisO;UPZ#?dme| zU3wO|-tzMn=KX9Hr;~eT<@E6J&V|(lBOK=Y5lCh(Ryb(#Wz$)_nQ}Ul&viYUPy{vkL*eGO`nY>#6Kv@cS9=7+`pI zrsXDJKbG>PU4x)%gcP#*8Z0I<0 zX9F%V+GJfHnNw8`7tC^tJK5+e9HQS)K!a;ul_s)%i`|@a!|!Y3Z`7`p1bE?=Ry*D= zl~i1z4tQs5H;oZadzp0ORt>*YfZmZaufZlDgYKibvJaEo7fn0`+~Ja11vCijTdmmu zCnV*2;Qq8;UDhVG%<(td;8GuGJkpg=Gkjq*V=#%~iU~w{M1e(N!Nkmm5gM?B|Hdvu ztjEP=`!gy{nHo+Z@;EY{gmaGT61&w5r_rAoL|uaTP=}EH{EG2d<9z@z)^41mKZyp& z-mXG>Wwb(jHINgX1g%rPh2uTX*&;k1W3YRiP=oNlJ2a-%zO2ddbO*@xknq%Zh~6Rm zV#U_H2ZZjx%2nB*K6%JVuS7mqT!F!R7p5u2eDK_NY0W`;mZrh~E#iGE8J$6l{eJau zb8)xAejGBGD!deg_g)zE?F;-(3*DhTll#j<`XKsL%F7|n>%&WZvWz1+4q0I^>RS=q zrH+s2n0r?(s?W?=i})3B>+r35sM%=J1t486_~xxR=A*Ose&qXchk-W>`Jp9bn(QMq zw~OGW^1&Q%i(0Q+%ujbl55ZmWBrKHkSK%@rAH_=!3f}_co3LMwGmn=L@`KO6SL;o0 zc%Am`b};QEUu&SJD=ap!kxwl@Ezic%CezK_Y7riKcX4^5-1V@%+7^!r^AK8B=QZTv z6WqJf*URC$cZ%c5<|XdoA|dg2+1uv5-~IQcpFFqgcRZMUNAi1*+|>7Dx)uD_>QP+L zesdGQwpQ+Hjs0Wx544_;8o)_b-Vf*XKidEcWkxJ-+Yj?r*F8PXg%g^y<%**`}25^7GKI6|c{M+TrFW zxX(+q9(~pt!pBo)E{qN4SHHf_>f*1RTg`AJ6xx=LG%}3^~j`TCz z^3BsZEB@;gP1cvW@tN8!Z_dx>>?$t*-3oTEFTwp~>gUddR_E<-@b7%{=QDZluL zPk!sy4SFWOmapwmqne3NYrW(1yxi{YV<_;(cPq~`57f$pJdf{zqi0I%NSE_*b@3rl zY^?zf^5de~@-p(@p9K|XXZ53xHi4OsA-`)?KE z9}LMmK}ted0cT{E$L`A`gDjmii12-vBUWrKLSWj-NK6WsSY|Zc^OnUG&-H01U2sv; zC=)NTfanaA)^nl8L4h_P48J^5a>qD(=FYUT ztGd8-dgn}@$rW8r+tb!@_f6Zs8^wCFAjj{`QF0epB^oa3iwYEJFCP6p)x^ij!Iegs z$H5AuSUpchgQh7X{NLu*ioP^6#fv(G#N(bh4m;MTyzk`!IqWo>W%9!o(=3lBQ&cAo z_KquX%14o)D(uVy)uc{?E;t@yZcf7EXeu(O6j&80ORzFB?QU`^!x1xP6kX(OI1x^! zLmPgx_;;W{){MTL{B^u<^vawItSHdWx42_r$)V2!DS0PK8(QAy4y6eun%Y#v+WjeP zoA&5WSt%6~5c}^Kt|RQDmIsvw(Z?RDJ!VvzVjx`#k)vIzg4E`^lgeEsDPftDR3+&e zq$Nj=N*@1q&fJpGa3L%E(9sQUW^WfLQhHAx_#KrC(Y+xI0KPFs5Jv1E4#Nz966Ny* zMbS7h()w{u>`_MTaG*JYrP`vX+afI6{1e;4wb}wV+af*P{6gJ=M0rM#d4`$0`lPx> zu)795y+)|LhS|MF<-P`NY2pdU+s9;*l|?VhGFFsmt@F4xB%YT9qMMR4!pnlYj!IQG zBss_Qf3xGJ0wmiB)OswfOih~Y7SXs3BrLfL`1AU zW-}r0(f{}1h8qeEQ56&bz}Y`y3HHA?^1o;${}NYsBPXl>qq{-ky*3n4K za{7}MikwS(dOpUt*RE6JQcR&(?K|x9|xv;KEBQ>*436EE84xVA22gzXGp3p=V zw;(H-0b(X3(O(Q0GL;k}Um`6P8kC}j9|woRmF}|PD`zTiAi&&vv+Mgi`pU0bdc9$% z!aFL%zl&$BwQ6!A#yRuMIBL(!>}*Yt|L@nfk=WSeV0`OLovZL1FBCDMJt-uVg_$pf zAyf)_!ksyrkw-dhD`g-sFGVKGV&FuZD-yXpshTRyLwB%To2lm1p=5RJ__+vewIwr0 zdnc-Np~ICWb;8st9=m|NLAWLZs~tg~z`!h5K+uCP5*FOw7Rt)M<|_8<%R{iMKRSs88Ak0x&sk(8ES+T%X9$49}2+m#w6@t9%Q^PR*uhX*@m| zys-%5FvI~ieb8&oI*qH%1^XRvZ>XxIIEvB`zS4q>m%s!m1QVS7yuT@hsyHKMOZ4)6f7YYc62>%PnVoHEITU+f zrwMVjIp2#c-#6cc*K8%U*G?X?#plntVM6$4KQIQu%Tw$mHPqu(j z7EDZGPfF1;V-9eGa?xL{vDtTW=%iNse>2YR)L=OkGSug}doyRQ3*%^Gl^aa#8yfu;Y1AoM5iLJk(AUl%IN3O(>ZHkEicRD)e# zq3qlmh8CWCNVda_@yY$6Ii_ToPlx~!@1#a|} zMgv-%qz)9$lWc5Z1x_|PSIYdg`ZNUCaW+pnG!Qlk;xYO+!af{9q|?4527JLI_z%Jf z?Uj3o%65Js?ta^L@$4!@Q^MN<0s6iiQU%HgZHf&@o$%{;*B7qm^jAsm6Z8w_%>mT7 zwZGw`j$SxPI`r&|p<=1|ve~=V1_dV(D#5fUI4CF;S{9K{|9a?AnzMXu42X^0TGt zgG`0sB7!aZ+^LV`&kJ4P1i3>kT>jiMl~&IRXn{h9UN^D<4)ZKX7$(X`)`cE4073El zRRn`v&}xH2&i>5-!Q^Rxl)r{dQ>+AU)`VskkVDsMLu@h?*265_<8*OBjGL}TtlT3} zEd&ToP+1RW^YfL2C_T9V?ZvhCsF022h|@w#VdVnw+4>N+ks!D(2B0C=i50 zurAlpAD4`177}>5g{%n=i(E6%1@pW@2qyG}Kmje>K&yufhf>*y^DZeQRHL8@C7AUH zE8}=1tc!2(-gKsZ(LqNXoN9$C2G7;mYwQuV0?IuU z|G@spQ9`XwkV0$&1#!I=*Cr&c3To1z5AYsKVCx_e#j^{o!BJ5&pt3YdleP;f$wQw@ ztxaV(10MdP}t8c?|g04G;WS}1uk}G$HO`wIKI{^hYK~1j;z~} z1RkDc)_1w)o%Bv3+xk@9?^S2Z?WXT+1#GFxE-p6r_ouAdpW@fkUi;1N!IkaT=Tb%M z)~D{ztJ|$pJk7>)|4X?Z)%&kb`fk@B|I8l`58*F(=8ac(>QdZTc;zbG?wW(M4{_j) z+6x!l4hE;?PpON;+N`#wCs{o`qwj=HczlvykNxXY_Vesoz7^EA!y8L!Xj#^{*-z^@VV|DqyJSG_%9&A&4UhmFJbvvpn-lso1 zGrPaFa&%LM2$g;f7U%PqZ*_eZ zY!vCnJzgN@nr5G>+T?WLW>3g3?wSQ~W#QMOnz&l7wph8iP-E~CQ@K5)ePfu#rFBoP z737;$SGPaRI!&&IZ`OUjw`c8z{*-VM|ugtelpy2t)#&yKhDyj+m3!l>PLq{7R5 z-t0_ga(6Ta)}1y#gO=PE<%u-0iV+(-kl@P zB+igAzbB;}Nbo_YRy(Om!4j`Y6?52rE^qT@-}l@&&m-D+1ZwW)v9UA_U20uoV-YL; zdCQ22zQ1|a!4_*<$ZskaVPRe0D6(l`v|e@s7KpZ^P~03K2}s9V4%r5_3(}KnLpp}! z+?P^bzk_m}Myx5nu>hXBfKl44SjO~dnCa>9p76d|(Si<`w)5NP8<{bJKLUu&b;$Lp zIfm+$@$$1G@jkL}fPIH*kMT;@ma8FUM~H?=Fi0RHHiDiHm$V^kP1qc79cvw@b0A-r zd5OD?^BmW8z^@;B9p`Jv{qmEJ!%qPIj}Vko0*{QJjGvI7ICLEOh{Qc1i>Q}CE>Y4U z2!}W}9@z+){daQS@&6+E#sPO|Z2$Qem;Md9DE~9$IvP5d+d2KW^3z$>RvKp%HMd)1 zEjy6eu{E^c&tO{jVhu&T&2eyE6Z0o)mV~2!ghUnF9>*0LXKP#c^`*~VCAnh2A&I}( zp~52ZPkmij1(k}V;s@ckyt3#;YK3SuKZ(k5he}0RbyCM49noZ(=jollcEveQHm19u z-Me2W-RYg2k59pTL0`RCvM|}$4bwnEJqSW&wf#&ci>eb8rd8VGY&5;>6FQ7t@*T>5 zRIappXdV?9br>#qrJsu1>Z28+{7ZYt+T4G8?^46z%Al z)uxU(9otvx9?lQCYAd1j#*;0WQ{r2qu?8EYD-Gbab^RzK9x=x7-(eIX5Nl3Cl00GaoUkMs(|@l7$&8SE2*zI1_m= z_B9qOCTaSP#47YnqBEp-B+$*12SnSG%O|nif#0H?ObGj=`lwkgi=d6?0Z{*fDUwov z)e7mbu1{A=0{7C^9zCo}Z%lkVmXH@p<548MqtiFFnVm!cx++q%z(&%q`jz6+DHHxYTp8#G7}fs#_Z zN)lnstE)qT;U<+_#sq7vs4t4iR*^Klm*Hs(Up9zJGLUBVR(daJSRZ1OeLqji8)a|q zmyy9;k#|xrB-Y8CFPixh%+s$ah_7p;J4c3zoT~M69Y}T6213 zC%RHXQtuB}s3H*o1s~Su;JCPwa0;=E)HhXAl)4AO+uBK8Zmxc#H1-gB+!ukVh^L|K z$9Nq4xRLmvlB)T8S|hRwy3!oYE{i=dB)sPg04}c6CjENn5Jx`MhM1-TC;24i1q)w; zo8}_39#(4}Xt|MX0r_v!z*50ClbBksHhm3?FbcYd&A*SZoLgsi7@M#Nf;2RHcz{ZN zz!3NEUE6*YpMNl|DSD494(Ap0JGZhyYfeR%+nhzaeeWt!kC^`>LsL=(i@!I5O~N@H z(hY|LbI|}@J2wKvfkz40Fkuw2G$eB`*f^s&A5pn$2M`Lmtvo8}Up}(|G=!$H3mvkt zrENeIW>KdjRY}m$vU&{VGpuFYc9|vM(X=w{!U|&iXSS#?%O%eW0&qiLf1WA`hq3rg z!BI#%Dlgcqs=_}dOJ)8pRAUC>s(=ni1krRNhsv7H2ZnyI1HwiAWEG3d6$vPbS==K2 z-*v>ULKf_GEX%|fV;LgH@;q}<;QcR~$Y!c+e(|f?Hee`A8hi(Rwza+BkPgt$2H?E^ z{M#+6-2!VPDs6x&aL^A5bFhfG$}R99Tz1_RLcD{^p#AgaPM?6_5HWghSj8%~+_sTn zl@{E8{R_Luiu48(A&rr+7QiAm3S$9y zr!dM>NCzedbL+)SZAZzvOgMhFwrnWJm%k0X?tS^av6)>rj_9&MI z?-($^ko!UEG#SPp$MuIE9jySSz+E7!`Uma2#;_fw3qw!4Wbk@0uf_nSCjq4|0e=ed zay~b^>!Dnx21WYt?!R(@xo@J|XT-bh(brDl@3MIMc0c(K1KkMi(^o-;yELEN*4S*L zc(!qMcXVmBv1iwLUR~n~^nQMpTIG7*pv7u`9@r-~wSf8aYquiDLU+FwZmx5|f4|1z ze}2Yh7jCy?O>6jnBrOkCwjLcHTTe5&wa(u0e@!2uPh#&TW+qvgQ==xU)pjF&dA=`? zhGT+rW#3|MznFI)Mv&>fPK)7hbI;a)@*eeYS7&!WM!?z8d%n&Mb7^&czoqCuOK*EI z+$v44dC%9gseL_N8PLkKJ^sZ9zZ5)0FTwcYLAf2;KPo4%*{*>=2W40?VxJ~Qt# zZ*{-V4w3Ow$Zx*?scuc_Bxk?v+&w;)(WwIEeq0XcZ+W@BJ((g#reX9p+g306YGb>| zJ6e5qDs5_YWpt;#cXT=SqfzT*ZMPVVHqma^TOW8V`%^{T+O?hZB2Z*7l{3rH=XbnkRgb{%BJ&s1cVwi%R>BmdAjCYw*u|QiLX8Mr)8Lfz7kQtz zG$qu?xpq8Em<&%~NR^ekR=}8d8*i$!c}%L7tacY%#lF~v^4e)>i3m3uXWEX~&%wnS zH~nJX&4eKkjgCt$$aqLZiHrk;CoUe@4~!vTl9`Ta{Nn_P&m=+{MQ9W05zfZLq!U9I z38YBGdnKz6k)bvS(IxU&eDHH2e9-^z6iUz``$`{1d$~Q+LVCAU|?YrBO%lWB77aQE~kZ_Ojt;0BPB5!3*PeE zQZh)0OV8e3Pw%d~n$k6~y}D_I{~VFRd-;4Jq5rGISOz4EQtY~dkEKmM$$;B%fS zcYL2Mr-H+AH=;noJjcb|eZA`ubEZQ&e4V`!4DMKKm$$@*2&dgo53yk(U%;q2X^ zOnD@s#j8oTjQxD()T~W`4G29C+!GU^_JXB5M7x89JUS3RDc8oyt<<**nj+j-o9?p#{o z6SyqwTJYX*s#@@)J(H_TCOUqJ0eLKkj*+fuf68-h)U0f&>1s4@oP$x00h z59b+d)~#XTmhBJ!ZeiS^yd?2inOH0Z;onrDR-2A~_th4d~u+w2CdLnUW|n$^Mu# z34WF4^w;vkMXtTLfy>9KDsUit4N2IOK@Tn`j%s~_9FLQU@n@`o0HuAQNs)k6K&cEY zWb{w6SG&gDUlCE9anW6Bw#k~>m5gY!{!9zBRM35DwMeQTG@)NQfBN;Bspb}&pJ6|M3O*#nzFm^4cWH$z=Pae>KH#KV|B&i;7GD7@;l z3i)H^O*;T*1v$5B-^VtX*eEk6B-7MpVIswiXKIZ(g)m~qUprBe%_o0%j_yNMze3M+ysN?^o-t|9 z4=Cj=yO~i9so|WZ_fU*nlBYPvo-Fyk$a>McVP{LN) zML+2$9A`>O*hXLCAG0WB`$drA%ynBCF)QVwoC)F$IM*ppS)w<)_~o^Tw8M}-Uv+R2 zI!iDXRE@&usksyet zO;*~auZKNsL6L1{P~V-!bgL(P#ua5TYqhJArKOx8wY*f^T?QK1v;N#a&R6D+IsI#R z0^WY5%=6ssZN`9W^AzeH@`KNFbcxFY4WW~+c{NQy2WEgNOV`V?Y~@A;U|M{0^&mxr z_c*gOr+UHPhf(BhY;g|*%?{JB+394k)5YD^t0n`n_Z|s`i|t)b3ce*Ik}{j?R)%Cj zS$Vss)pXgp3Pj!GNy}iQQt`~yM55lMI6WR2T-C5#PU_l9`e#aDJVD@m>DYh+Yi{AV zuLRCr*JsruFX4UJB2b{$%Ye}@fvdsMh;mw1bi{5%j0oH1FvmtDz?TR~0gMq(5yqhD z*{TVp(sIFW>2P3ysB3jxOxj?$3c?45_`JNQPEe2VoNeB^fL+k_FXE)aYb0a>GBV?! zY){1wO5zY5nPEucmwflR5$xi)W<}X>6{Pqy$ZiW2-Po-NZm* zqss7gnmukNIxIwFt>H@ae(h4WUinHH7NfXI;p%uq<0?w1A28)Q4ue-AiRE8F4^$IOZ;y zZ=}_`h1`%bS+KLbip9XlEhwY^ZslL3-qm51uN{QddrzsYzFX`5mA`LTbcSze84L`- zY`kIhT6NCWydNG?2E0u0R^JAJm{821jdgW|1$8^J`U%!yqUx>EA*zI%ED# z7dL7B9*JH2AZmUcHz3B}cq9g}`>IOx>vxwse$J@stj(hgs4MX94j!mz=GwIC304iL zEo9@X!_WS*gBykS%`ub;$fF@z`fEB+TAgZ%R4dM!FFCqBx>(B4>WcarglnpPmeqyn zNt4z7OCR0cyl6!<{b2R+(QYSg*_pos4es3y7J;~LIxCU z80iFE=bO@{v~c`cEFz(Q*|a3VEY`fkG!W0WL86n!rUR@8RWakhI&g$j1hE);z!A}D z0yU5foS#0624GOO(9ITczJEYBz5!wPlQF}|A=WpIH4yz7_w^i>|W@`t};{CiPC`66$F@!A8AkM z5^eKEtVD^xP+OMM7~s@RRPiS(4oxy6c;L7k>OeQa`@drbLOQI(V;fX1)Duico&Y1N z%eg)P1%w?J!WJp=>bS;R%l}2T-mpxWs%tF>WQOXO`ThEqC@F9s>4q=5OnzzK>e`kZ z@%Gg+xVNHF4Z9iJV_SiDCOGw4;&o(B##xNbqa#Or{cwFXq5h|z+}79;V-Vi3LiTG( z@3TTS-HVZKO7M|fUf^!6&@OR9doysEw&Vgfr5rHCaNMYD(s08v52LBA5Lp zogkh7isE_tt|75$fX+cL#TzSU$^zz-d>?sLHcL_rqV+r=epO?4yi{2j`<|#RVK#prfGgx+GjRceGvrk3kP$q9&bZAU2b&)E z`8HnsGrt6bc>ydvCB_4FQi7$SUNE#bd8T43xbIu-RM)>K0ADFY{)MIQUvUF?SSO(e z-1nx@BgE(PZGcvIe=cV+UWNdrra#shgK@D=#PiN1Ci6#Q_m(D^{;(Z@%w@4y1U4}8LW(KhD%CyV_2=l0iDH{~Km)p}5Fxt+4nANOv6F3n zHKvtg{?tLupe}{d<#(US1@e=>H&($vN}AMp#s`E(z8vh|0)hQ<>%oN14?uboUVvsM z#H~uBd<&ACK>$ha=lW@};0?1M-y!xtNQmRnjCTBM8bimu22@ZAh&rU@`u&n?68i=a z`4-IHzuSZSDg@(#BNO@lWqcrr@@FuKjuDNCj=_8<;g6SmEIo%S&AQz|7+zuy3!(w{CqEv4O)>z#u1gY)9N;65hpt#G z10-rkNqNOqmzk+%Z;3=MvxZQhy>UmmYM2N5Hl2*ezuLxwVWf!Ok)NU2K92th0#TRa&!$dqK z8o0xmd$q1ibhnZkqv|uk;_dh1*cS23ZfQ^5Nj}M;c{-5cD`ePmx(0UPsx+Cl+LBZn zpM_B6$8TPj@8b43wUDd>d&(9=kaO8kZ58`Y>Bx>UZY;LR6x^N}_*_@LH8sw!O)-`d z-~jEhmed`QDOaQA;=nwUC0#S`@Ok76o0Yuh7&5P9?Vmn`q0^q-ccj%B>Of9#VJoVJ zR-XaQpniZOi~shTgRL}R&k9aPDPpS1vxnaR*{%fN4^5(@J1`0cEr3H4FtPf zG)E>!nEx~ym>+w5zWd_IAsp<2o4kyS0Y6RCrzbL`SG0eg7=;nJJ2Rl~Ut)oMD2j(i zmkfM|U1qO>sCPwv1*_7AXrpg((1U2Z-ve44@q2L);4c0cmK#po@!X8$)@%{L^;C5|)!buRjqwYoc=Y7c?juq#6QXCIIL0*cGg0YBW2-4me2{hvTLf;^ylErdP1M}WTUs~AzV;9 z^@g5|-)r2_$u}5m0Doa8VnICNBWn#j>LcU6`EFL|p&sC|S^{q=!_hy%PM3}W?H^{?bmUpjb% zj-4Mz>prR1;rNKfkKBV|XZ_=UlMqA~dH`lO19Vbj#?dI&Qr$cb(Wz~NY!zK3>vH=d zsqYS==@LFEReg!q@cZw(8R*RywmOpcZ5Ql&Mftdks`)edJI{vPk-<=G$2Pa|CoLdz z?QJ1n$mM_OmU_DbdrMtpFg};2eENOhI&msS?ob~cyIMd$m8^a$c_Dw`mbv160hwPE z*v<}93E`B;ru_%+%%){4PTEW+3PVBem^@AC6FyASV&NVqC8m%6RnXR-T<<3Dz3*9O zz9!A9O0MVsT{Okx%V=>((BUuYjo(eSMd{3#W_j#Ag);2V8k|7%7Sv5X^t%RswErNY z{Q!9iZmL{8`&{bYF8a!u4QuU(XN?!o#oJI#^d>$30Fv;TCBFgKT5}Qas;sGuRemg< z$wGOucZrbFX}B$JK};aTYj`=-Rhz`6_d?lP4{tu=UYVpJFAZ)=40~e!q+m52zZ!7q zaPj%^Du3Zay{t;Ck(V9BhP;QK$bM-&oMEy0TP^~W_y*H>2h0$y)%v%|q2~w%e8U#h z^&WrP2HIA?u|9w9^riI%x*rC7^RB=J8O#SX5Dj_Jtl8@KnKC>gcYGVI>-*jK`oXX7 z9s5fa>ir9>ZGUtgwyv)wI;nI0yZE=8prUGECB79j^_xFm; zwUOn=@2`$zIlZNzU!gVH3a>Z25B;jw`3A4h3w~OK`IftAgm^XlklcOk@*U(3?tvGb z|Nb!kY8&Lf-d|oY`~rFU z*!WmSf&!0PI2I~m7!kxoeA)bvoZ*4sQW+TrR1=a-ItX?Di`Gt}J zBs5xRloXIARP0E!lFXsu3$;-!6(@-Bvd*D`V+S(&{tdY&;$mq+f|JhWWZWA2`+mBM@KyY z<nG*vL^hR$s>Cn)%|y0nN06^;^gVktmWt7+#31^3h<4PCG83$p!y$x zi@GQk*TzKsB^YslRZyxM{1aus)-8Zabsz-y%e{l$+paGXD+d zopsrl1DMkB18l-5VMoLoe_>$I4H29hAS*iIs1`!y7wj7ZOWWB8c-`Ta*Qs6*J0(NZ zb*kpg_julIfg9vMt7$fDkXsOT_$$m{gSLSTT>BD-X*S9WlJ(c2QgmGM!yc1|>2ORF z+x1bi{W!dZgSWGKs;`FY&Dg`scW)p)8hV00f}uReguM|~HP$8zc<-3FFqEC3a@y6K z&~>Y=f=IdZo=U@g)9Pa1kkhjDxexo-;MvfwAN#(B3chVZb@uF5fS(dYIm#keAi*>J zV(s$eIfe>=_m(u?(nZ0IagAOa%k0+V#|nh+9=M^!)t%InFiYnn9l;#Q@=y)#7T+;> zWgKRdzp;Ed!sdm}v?k{^V74iby(LyWt|pq z?ss)El|ltC(r|%tmYora#F);TLvRZxZKvx7fZhf{owm`WJ0d7@0q=uia`RxOUijs) z9WpmWR)VvkWj8~Tb1cZ&?3@BzS(mN3>(ArXkWy)ae8K-~PHhB7Hf7TbaO?TGph$E_tao~qL70I6DouhS3YP2Gw0c{y? zypO1F_h$PJlW7yF8&iD%)%k!`IdsZPx~l-xLIsLWUP1#Xo}_n9vH~iib5^>sl$d52|g96ft~ljrAqD)VU5{a2VZi*cV057*p!)Z37`ND zO-t;932g1r^o{V&^aG^tFi6Ce0GEVIi5@3*hZe@bB{YaTYrqy5yRna>Ob%o53|^;1 zlmMM5oGoQY?4#-L9Bd6AU`QxB0W?Xmog1Z&ZzrgZsf(iW(&W4{2^;OfanTYfw@n+r zGyAz=(-i64l$9aH#R5L)0FWH^^HQrQKU*|A;WW_#gA#Xpq3%`L`PW8)2!G~8kO50G zl6*@Q*}+5XERuP{C-jucbac?2U;NUae^PaTrHukUu1f6l>FNjf?}M z@isyB#v9?4Q{a~6gtYuF(tU|;{xB{b*LTvXmO*=+;cGpcj-^frrtllj8_Lc8ju-S= z4G;r84FNl$Irvzh=PYLB3eL<*D_YOw4=|%c`{H;s#;%1v1p#}!g2nk%kbX}IBOmaz zLdp@CV_n*!k_tsRA{++myoHXqK{D#D3j9uE>!-=G zIA%Zvoozj{I$DLbPG5OeH4Un0`z zigp8D66v24PxGLI0&S`~$ss$8RQO}EOS;JA$CPYjYXa{MxDrpp_I3MzLKs4@vNN=` zT1x=C!ECY(iHq_AzB3U(yEM{+=LM0f1I`!cL%f;@X4cnEUkSId{@Yn6(DsjrbZPW| zte5c9CyAEp;G6j-Wyao@GGR-gPhYQ{yP6GoTker)G&2zXV$~v&1!uD+37p=F6A2g? z?nkFAijVLR7-HJnTC=fH9Bm(^4lTdwYp+Ku51-x&BVbjD$3>U`BZ1a7JX=Dp?cgEco{W+&wkq=Ev^`qO7_!`@nEi1Y)6%&gD4`nS0*JrF`j z3KbI|9}0n_^Szl11egG3o&W*scP@bD#BM=!U*<^25rZWm9TpOqbflcw!zEIqz0q{N0Jy<#TKmY+k&iz+d&FY$W8J*wTLNlzONW>}lft+`kO&*_EoY+!mm9@0@ z*aq2BD39fzXMs2qtlyI*S&pW;Jt-`{Y6??i35MrUg6lFoC?v7(@$PipLBS2nFYbSZ zQ7uaF0@ngA#L^P7`V6cdnd>3({1-d|dn_FCWyu?F7U#3d`Oo?0=5~Zm=%Ro65C0`T zj3b?i^tDj)i*y0!1Lmv`gina+jn}g1BPPW`-!L(`)xD^~7n(RLnW|UTtq~>ff)M8W z^rCtGg+00Xwco;ex#ACUf<@LZ{>3xqGCpV{w&z*k_pFUmGbhDO_a#%lF(8Xt?^v4S zUirgE)@7AATUHOgXt^iRCE1--q>`I$+U27L#l%A_3-T=}icn{QOV*#=-U82gE-Ryp zk2?9o#^{S?&r%oLpSM3-npxf%8$LDjz6)>o`7RCOvW(8WcK+p;xl+>GeBBdE*Q1sh zTw8N@UB&f@n)NgHMs$p9Bh0Z2Ww)=&re->t3(h}J7b!OFm~|;KrR^(jS5_2CpWF-t zP(M3Q`VDD+S2yStJ97?}H)fTxG-sM$4wr7#CX$O^{#yJ7-%t5#{u(-c>x?k#Sze2m zXWv!d>R#5PE8q;En}5M8ahFrp*5enrD7tos%%WwS!o`7M{mLxmK1Iil`~WxG7TngL znF1e6=6tSvj252=Omn6!?>vsu|2hA)IdPAyH(VHgdhnCA8d_3r+A@RYL;j^X;N!13 zn0r?l{!{zqPTI3sc&Nog?RBX<9l$#MQPtV|_?~!HF@4ZHd=SRp%RZy~l?(UCC5au| zl$)rm-f=86T@NWPeA*m46FZ(b5NU;;Ww_Ys^){LZ+VaNfULE@mmZgQFT=!RWEunK0 zSd07Ao0e4is4^BNLw8vi7vo*iXK}4H1@_XbOHxGX^5mY=aP%@pT*veDWvnmbdbQ-~ zGDH3d=gV?>pq8y_r>8{b>FVO{o8K5#PgjArSNtJl`l2uAn_e4d`&qEP@av}o)YaK@ zwU%nG*ZF<&Q9;S~@pDgn^5;&8kaK$_=9AT>?^^sH>g$%>6XnO)>pWkqhkLMBr^C?A ze(TkIHMbUP57otaATemcx+R!dsG4skdb0=8Q-Q<#dbBht5p)|(x=DN$LYNJ#huQkG z@DT)1bz^kKw_ojwOSP33*O$lCaiws?-7jjOZt8S>(=Z_RF?v9TPU z?p4<7zBnH#=nU?AFKx^GXb%@9>lgHiRkYp>p_T=y7bXb4_BC#X2-VphA@_;Jr zr33q%i^Wu5Pve7*C-z3dV=JC7huy|lI_y7Hi>_CZ?=6UD-`)kKc_teQuU<#*aZyp~ zCQ@;&T6+gNZ8_zK&8C6Y*?xD-RHwpY7IcpdZ`r9|*G}lfj@5}Ryj9}%!|PYEdni_I z4`P#&u8MkHPah_3u9kwvwv}^h(e3 z`sY6w2Ng%X!|9M8o?|z>$&KymZPqQgJ8*UR#M>$pR&Y{lmhZ=7XsRMIT6X%ToABT! z{i}#852Z?xCRNEcrlGUO2Oc%Q>+|qoLcS8&m5pNEm44Q8YyZboiCo}K7TjEt8Kw`x!GKWLMNwh7WY{`fx@QC_y>2B%Z@h4r4M zj58B*Wf@(6bHc^Zmy+x=KwFAJc2;4Hyq5!5r}Xszbz^nXh0#iqe72|YOm-pj9_W}x z1wSaKjOlnT0eRVXKXs!#_iq!k9QSEcqYU?K6Z4fRR^*XK+yzM~urj&(l_^i-E6EsM zN6OIFA8(g#re8KC?@Q`OIqt_MM;qE_JsIZ*;@z9$r5+3;Or1~KZt@q~GK5bx0zC$2 z)A)AfTu3AVXNR2kqyxD^A)lkKN5)}7)1vXE+^lTAq7M+NAZE^3Z-H;|z@vqaILB=a z3%R`6v5kC!!ZO*JI7#X4cL#)La&K(8YaKz4MRi-hr=mBP3S>|53-PO?FPy=p?E2jz z_xPXK0|tBzzxf}Q1lANL2+NRg@4b1fI-6-9@wxR2DeX$lh)>{w zdf~~;nmRKCqp;UO{-EZ~HkTfRkP3y65((nX7d8rosH|Rr9Ol-B{8RhyP$3PBSjmw@ zD}t2r!Fke*z4TS7p}}7nYjAzwdZ=Jh5;u{l0xxUxGL_te6pR!L$s0+l{oL5}#DuiC zi1B&@ek{cJy3tW_N6tJqJ9&wZ+F0bTYIaIoLS}pha!x`rU&c8T5qBsG1Dx9`o;MGF z%F`up!P_I;&baI0;aiRu{mwA_(c+sEU*PNs|3T^9;#>I_`Ipm=k`EYvWbX9oz2lqv z7myEPZ`5y^-gxc7>f`P`_8b12%omyub#LJA`0dH-J@|X>JMovn4~5=T?(ys+sTZYh zeD9Fn{^XlOPeScM<-6sV%nzDx=x^|EwC~vOIRDA-iJm@~ry=i2z2A2oB747J8)u3g z^%h#|put)M_aS>-6z*EM;r(VCc+BAKp6k6l4@~Kv5f4=C(7787UgYRLPF5s;U*i3! z8&ysW{~+_7G!ORa{_H&t54x{^@6hg^R~H2RNadYo7hLTq_8r+5@lLSo;rAN=Ul{y8 zkrxd5A@UmuU$ER^;~NfLfAJeoPuQeAXw4Aoe+1pW!?Vc%#pw$|=EZ4A`o+x)s*nmB zl(;CBNW~)5wiG%^7{mEaDp`NnNmUh+IEqUbq|OVURJ11qjN_QpF-u4&Zz&6tSx9xp zmBkKvQN`vdpHaFGDlwA9{wR4NtLPDyTqq=eaK+O&)*`c}Y|Flu^ z2gn^HzN3uX`8o~x{vRh9Vj|QA?Z3U@w0{M;e`K=%F36dfTN&#)JDFQK{(lCbw;Jrf zY8>jnYFxF>dTN&QgB;E%r&dh1NQ(D|{omoDtn0A|Lu8=-IU&K6;>K!q-z-^KCJ-_h zei{EH0cM$45G*KvSt+xTCQ4y{#%reu0Nkl2GUmAuhQ(hvC8fM0|09C*{m0 z$+76*Xe#WHA;hXrRd!WR)gPicj$qvV?h1-kOs)wX~Z^G9=Aqn9~1` ztaAzyEoioM+qP}nwr$(CZQJhCwr$(CyHDHho-;Es_s%~PQ4jm6qN1WABQy8PZ>B%k8cCokGP`b^PGXE> z0-Fbv1$=-Q$mqmi|FRu=niDflm3GGfAsj>!k|?V}1j7`!L5hhNR3rkDIQ!zEf|8iR zDbjY$_(wMamp$%U#AX&F{LGLTe9m~(GJEMDWqoNS2?PS=m}yq=E!twM2-F{OpC0H7 zU+yc07yPCSV}ukh5yN`>H3>Cf75feIqs#?Tkq1`0iDTpY9Gn+eRTSYgLVqBw$ zB3+6ionmsYra*^b7I0E}6p28aiNRz=k-R~pJgo#v4pA4%irE1+bgXNht$;{i7Zk!1 zUEtW7%p-AxRSKJ_eu)kz3gQKqc3*5>E!Qk%1%ATRD2 zOb^=A8bxD!eyUD%C22lBK~rPf1U)YFjTJ#p+0#R|$XJ$q@7%2#8HMu-jDqf1jVOGk zl5-Ux$JOn<8Hp22hZ${_kwy==YaS;WyAYqMK(|oJI@pI7$R(f#Qb*C|5~drkz8N6x zRL_q$=uxl%L!;<~g7wytVasTAs}aXO8naI1h!vM5IPzK0FZVaYS zaxq{W1};;dj4biH0!clz6}iROo{;pZ3g6S}_PRT@;<)ujoUsPzoUlf(CwK&04u1=P z90nHP(!46nJeX#q!uyg=$fRRt-pS9;lp}2v>A})q6C8Ywuomh@S3;(t7enA`SuV~o z#euG$G1q$a?{%fO`O{{}C=)@g5K}c4RdxAW)`ne_q`56yEKbMVmoQV1s*^<(C|4p) zx^}&f)v0#4RcL);1(rlM$OinnPI?7%?*}oZ4>L9NInuG1__Dx_!4ttU0=5ezi9nnE z4`Ul!8luFhTe?(ps%_Gt(?k8r_!7*%X$@G8%o`;rL51s0;6=3ulq)b+qhQMQg2e&C zdjUF3IBp4><9`K{q0YJ6sy#r5X3vZN-1Z;&;_b9bvPBJwBD90#E4F!Gj>Q1 zK%j9ioUU&NCI(*?_?cGhl&zRqhcp7aaS>eE?Cmj983lwCRlS!1&J4)FtS}=n>Z52q zzlg~Wo29Hiph8u8=t_pHG-LvC8kpRE&R2WPjU7=<~86P(7 zN2`D=+8#{H{=KaC4^y9AbeT8zeDVa(dk~ z{fX@XFbn@Qg#u}UO^tn<$HyhGRr4`RE7uhKO!(5FbVo5)YoSosCX7~N04tovi^k~} zH~zqR+i|+~r_8&BQrYt+sPu7v1vdUUz98R0t%qU5%`Wjw?PYc?aw|T5SKqtm7v7RQg*R(-^UH)dvIUR-R8JF*2nTE6#9d`-9~@>i$0DA8`s0~ z#@b~mp5J92IFx?xU5h!M|GUg}g{$@6&g=U8v-s^(&vVv8F|^H#Ja}1dSsw0oyLbGq z;*|dM`<#xCP`jvy?=k4X+lGDLM#snV*e?yg_b>UY9!GOW(b0q~{3+h&oh&@Ep;`|X z+95hGzMs>}OQmd?+*a%Fw>G_gn_T8YEVts1kINY0ee5l(7as1?*ug?SitxTJb@vXjGAM@{cbl*;DO!xg{vb(t*?K1yum#x>< zXDGi*ulC#G67--jy8qYY9H!Z;Fa6$EwlqHN&ri3n&kHrbZX-EA$HtxGlz0!hmR`?w zFt=O}d-qT~YR~K9LRDe=21kwf;NMp~>OJlnEJePrOs>?mQR|D4fK3`tZiT9mrG}E_ zv+vH@1-~9*QPQPWkYKY%Oy?xTjI_$z@5eBEwkw~*O5k{&*NZLJ&lgxYb27tN=4Z}I z`?=F66Sm~Cph=LSY<;-}AHv)?OYjOI)7F9^^GTSY3giix)cOg`(Y)zl<oKZ+C&2!VB?`;?xHmqeZ)LNwNKN0{g+Sg;@*Tge2BD^yOv<@;-Rka+8o2m z(xgrll@pJ0@m+JHEY%jCfGk6W`l@s(4&F?)GHUYu_HQn;Xx5Qo4cF?Gkn}mU=60?j z7x(xRLv43Gb@P>*ZnRsrzFW2SM*3B{H@KZO{=~HlKupjph8#mRKg&y;M!7@6+JK}Lj;S<8giH{8*Fy7(*jiwR)^Uifj%ORFeHXnx{f6Wa04aE}p z#{N&~J2=%KQN%y8^g1X20Q!IBmo{dm{|R4kRq?e!HpTI)bGP=k9kaX607iGSHfF;u zUbBm_jeOkPSefy?$iRuG0EubGyoW~KyR6O3@|bXb_0HdW{MkM8bHCkMeXNEK2#VIrFPkEUrUd8* z63J9G5f)@jh^Y^(Pmpq!RAaFaOq3GiP)s`Dosdc+wba-r)>+0dJrPJsHrYbLhLV~> zRbU*-ZVA-d%%G1C1r4Lch;ls02t|Jb;D(`g@M#V}6%`z2r58d8oo_N?f>3snK?#d- zKtNb*{DZ#HOfZ@8SAv{mnlf;rUzCH72&E$Jhmo#!g@$9NB@sqndSwvZE))zvVA4P$ zphP;5B7{N!B6ME!nnb9U08;C$V22HhR#5^N)B+_MXcPXg1^5vO# zrNStSAW@3YWp$k*ge@T@MIg>#F#pYf#l_jwt z@Sk)x%@vy1!jX(d*jfn~1ioZg*S9ZVI1g!V8;8O}BvNYv?`)0cOi@XCC>z%5ndT(6 z8j+emu$bejIN_+0BsBoJD=JV4w#}%j0_=V7$zrZu0s6%uFB8*H#IU2llwWoF#(4zHa{@bvfKV{T!U` zHH=+8_sP~&K*k`IsOM!hZ9ih_Ub4dK_nmP1^TVC6R|W?$nUS zvF34VyuZY)C&CZZ+7t=D?DcTbQ-tH5ePYF5W!h!*Y&=w!2>G+rSNcErEO|WgeRiF? zyW#k6jN#*A>(}qSKj!uMJKDGobr_qI;s11Yvpfz+8^5Ir>-T6p2R(MhmGS!jk^B0{ zb1rLlof6-!@y#wR-5I;}_7|bkPId=4M;Uzg`}?ybUyNpqr-`;=-^^R###?F(z(sN2 z0Vp5Pli!=K%go;KoB4+Rh>TAkH+)oc?E`Z9#K~Lw7uMm^35dQTBo8pOgBCx5sX|2o zM-kMEkeE3q0{{d2&?8l_9fZNbha=Na18g5N&yW~rj)@WF6ay@y`};aq9*nKk{T-=b zT()C7Work+fwg1X_1QD=zGJmlz}sD09w?(YM`pE2rQMPjbrp)meF9U_^*HV;*zp$RNUyN( zibgJ{^3AaC^}g?VAKhQIY`L2uN`1A|YxHWqd3tXU?Pjw!NL7{0f5I>v-A<@(%uP^Px0BcmbPC?LD;QNLrU?A_jfo zhSX3>mD)@GhDo~D>#s%r;?amTRYDr1FMz>=P)z+(H2o_{*Go7FB=`W72vx%Vz(r!dA4r%w&_EGvdB;#Y zuS_)~)Wni)TtOpS7Pkn4wpcB(IAQ?*Vw7Wg;=;BrO+^(wHIP@pAdznPNm4wikq}JB zLTnNot`O(I;sN&zLrjAs-QR0Oh~5ow5E2qvNE|gR!RCQ!=g}=L_BLw{`PL^xRpo<3 z9;BpL@4({$3oReG#4rSclnb#N!~iu2k$o~aArP>~SR%miq_EG6;`C1wcB4=tHRfiC z2VHD#DY)A+UE!2=dXhWU91zNgdQO^o>O5T%s!MlL=xL{AP*Lnd115wuJyvk%n2Ofn z$+%Xt2$`O@TbSJS1cOGlj&#^iwvjYECfWMu?jA;4>HSSGpdSb>OTh>esX$u^8`@kL z(WlI#i&ysoi6g_^_Z5%7e5!^Ge+vK+0sINj2mp!$hyYPu{DV<|?oc@3DEui=`0JQZ z4`5%Ir?q`&>JyUya$NkjjA%9=$$;a~Cl_ah2=^m@z*4Njq6XvSNZw}TspHoZVC$z< ztwN<)>ZIjcyoc${z~J|cyJ}~SzR&x4u2?@WZR$7ewk_Z9`)G%}k1wu}TK~m48MSP5 zt;vFavcHihLnYh?iz7gr}28)Wx_ONzFdw$7*9&;I??2pM*^qRIL1_IBv=#yjWpcKB^@ z$fx>kE&X1nTUX=^YZ4Gr)!fsvJV&!MP=l8#|5-bG%=;0j{zQf|$wNxt(UW$RI24lQ}Z5e44_)OtvYIk+ar z@fZo=5r?Gj6AyyRe{ekUJ8s7{WJ=<)3T{k=iB>+{Np$6bD{9KZlBDn&ZJl0gQfBu-_A2v9QI?8%jn)c9X>(3l zoqQ!prbOmnaZHZl>J{~+zob*ubsQzGwApixoGX=})_Q9W6Z6s|-IkdcF%y|84b@)E zR&1(tT71g%*p}vPTddvv*sVXq_#p;6o5u+BBmZ*wK6iV`dq_w=5KtEU1tm;6iq)emB zX7PFmEGn}Yt0=pdwWj4EMo>V61o*PmnOWt0b)>p1GYidBg;y4>$TXFl$?PlZ3o1VU zDrL$j(N->1nyr{F8>zkyp0a3ti5LtUm+J}BqAFXquz`kJVCJwsS&m_5oO?rV%_PSxzW{`KAWaokiOy`$eZ2bL!dA1u#0^XA6TrXvnPjTQKKu5ntO@$Gb5dr zJ$1ii@TLxShtgzol8xI@~Z8{dGI0BlS0-9)~N zB;qbh>4??ou(UhYc*>ajou4&dA0jDqKo)BmzwF3?=L(o9#VU9hEf7X`&}|q#x2F5sbW3U^FLAR1Xmoy&IYh z>Wh5S+PiOoB=Wu>PR9~WKtYQfUYi9)i4^sN*c@!2Fd*g!xCjuf2n0yNfD{}IkbaI3 zW=+zmMJ^J#5E87@0g|J(*|O+pg3)&L!@CJ^_)9b!34K_OBlY&rlbq)mGIkOeQlA1b z20nxX;u-uqp>x4fo|N`H{4<2$OdFLTfRO@;an`fl113Z6D)m${6k=J$6o{2urQn23 z3sAK>j0!7Io?jlbBm~TM`4ANyr+R-E>I17V@+_bT3SJg~i=3+_r5}{cTo285d30(p zN=lN=O8mhxQFMmwZ-DHF30lpY2gbYpt^AVmNjeN-a`ycZZ1FcW>02>_ajg@QHd zUN^**i`Pl?UF@)QK<|i{ZUP6;x%OpbFyDE>ZK-m+kR82`p9R?6*0qcqBt<4RLF5pU zMmLDorNJF=kt#s%oLE&4(ZiBTHwa~MfKlvLS(G+>`}2BvI(jG_b`9t)fJ>+qQ&P_2 zM?cyX@+Jpbxue$NX-dZ|wVo5acD+!hO)*0VZ5hoVkUV%TYJ^oAX1NVS)D;2XtXXSx zd7>yawc5T)sY4DZCTl}E@_zrMn8pPbikjndBgeEOYMwWsOcPOcDRVEDS+^|2yH=iKqE;D=$EEW$D~#gkKz%(3AtXMl^! z5fU#bZ~Y0BJDUfWtejnVTSPJ>cL$n&=NYM$-BBA;t7w z6dxppoCd8EcATNB#+aR_a4(xq5cO6FD{u+g2=rm#Ac1=C8sG}>K#5X7lguh`!L;Cd zf*As89qyT>!%g*|aXUGyTbFAji8gC};Kid&Z^6~h7gqwqSN_nym+9-D>E}_{*r1pI z2Rl5BYrV7I{FQ&xX(M}lB7GeoXQ_3(RqGDBog{VhwSzL-KN!X%F8eT#|B`ZwSK#HB z|9+2#E45yq>ceK%@w|WO_q|T{*?IpR>==VG?^-PiE8D}gIa=+C5zqhGI@E=I=5K$E1mf~JzsNfM?7cT=H{S{$>-(LK&-1-k zQR+JS>m7&Z@3S%$53~ERI2!C;+DMP0xHBl> zJe5fse&hFgEbq=|&(F|hbJLLptdH^Y_3k{^^AlYGZ|8fTY97Al@n@p9+IVfUIMf;c z%W*2QE$(Kr$L--3eAl;2700*Pw<@pnTN410Zy~>gw_|eUQ-H05D>pFKuCG&o$j+wf zQr!1EQ|o#YduL;R`w`i}wX^R0RPe^v_0*>}7r&Jz_{K9J&&_Qy*{~f`+1ZXIvE$lZ z-q%rWj}L{W72O@zii3f*#ZzW#Fl+AqtfH^&VbVZJ*ftC?=j`i`W?r5gCc7f|hYg%P zC1{vB`2$u<{X5XnjP->PXR#TE&g%XOQC&r*B-|)YnX)rovw@0HH6S>l~r9vJ^>-@maoyxq340|$8w zv|gvJLa;I?oNXT+tzIuc9spx^rf*I>5$yX_3> z_|yRJ{|}R5Heuh<^k1%r@-Npz`mfs7-o-)R(81w<)wizd@HW`ysC|!|_r7v;yq`H_ zmOr{jU+zfxxMi}NVR}iVcCBo&AK)Sz4zn*Mjz*u|UCy0uUlWThf7LRi4FAGa(js44 zw4sIyQ8(9WkE$iChiHX>P{Tk0vlvusoFcucqP9?=TZ*AdD2lGwPNtY^Ns%2U(Pht4lIF=yYB|m_ zm;M<;{#@|Wj3-IWNO93HOHoXfVJf`HvIGW^z?Pm=>Kv{lDWb-hi%UiyJ^52T73!pw zxYWs9Jc)pp%vz(MmaOE}H*@B^UvEh(8;Fw8|CHjRu!O2g2c*n|nEVuyL?=U$XhNjL zQCm(a>5iv>tiUEGc$D6pl7xF7VwI<)GE~M}I&7*HHAajnnXf@=s-?u5B0F|+FQOI4 zJf%*S!JCMut`Haar!gN0@Mh^B?)2y-30xf^1NFv%j*H6D3~{6#I2jPbERe5|3Va<^ zK3XWeXp|Kb0!KYn1q6Ivi>iYawq$v3}LShSKli=eOWeYXBYK>)*8;VJrYS$f9~gw-iz* ze=rGn2;oLBYv;9l5`CIGx-N;8XdF@{ykPhG5Qo%F8C_arcUMtxNx>%vx_AwF%%EIz zo*r*W#V3Vh3O6)dnl0zBO`%*$WKYfUB;#+CfoJ9FHTSS=Q&Gct>tZlo6!I|0NsJOt zJp$$_x^I*{NIP@@{U;@(GF;*~!8opa@SOH3XiD8@u~9;ZLDBLvqGe~24+>7=%?@K4 zdOF3?m|%2KfHq}No~8a|@?q!{?fr31;qGLjvsjpJIt-p}J!ZM}Z8>)NU79e3?TVd@ zp*YWiS`U5+GoVlB#jjs+1MuNsY;N$dpKu zo)_Vqz#~vbkQh%9`*DrtCK=Kj^94BuKDQLy!=8f@22hTSOyg<;8m>2(da&39nmcXi zT?V0GH_mlTwSa)x`X(lsNIHl+BfH#G16S^0x>2`h)pna(Dr@j zT)}j}P+1yD^|boxbDNi_S+v=LDML&j8PpRSv>_n!&HYYPCl!tm(S@|&Y9mjyNsQ)T zMj9v1aMyLfbSVs%lHd|UxD|%Azh50{z!iFvvH)VN9tT`jFr83;&jS=!-I#e+#67va zGq7&S5b_K`8}_7Op1e19>bosJSs=zQv%yF%}S5ZPkF8R)5zg7Mbjxj}bg9mhO`aza&jZs>CW zZFy%FaRe8i!F0hPxd;V=<1LSKW?%-If>F#&!b9Y7%7e|HxW4roMG5ZaMJ9|=GX!#v&> z#?~uv=z9Zh4MG{FIVN@u&5(;}1{?EP^aS=ByP9TSX?aLQP$!O!=J$9;d?3QzC4_L|S;)DRzlkc+H zJEv}QQXP6wn~%e-r(`YAgVw)#P1_4tqPXN!V3WoMXDjcFUW21OOhAuUP#Qka<>UgJ z#$RV^w0qb4!D=@pyy=9vbLQa}WEK2&XgjODeQYMJZf_B?O05l5-6*%zM*iG`PJ!I> zvUlk<=e)<7V4u^=J4}tJm!_-d(-7xA^zpy|;XS)}Z5a zE$6+wUip04U*dm>YW`k4YU_V5rRL+(+Cuy~c-)n}%J;7AF)uz0e0c7yv$wTEKB}HI z!u!}&zuc$;CF6gcwP-i>y-%RK(H53m&VAT*JDx4vm@lc%>3h{a?;h^as^z55_G+@E zOm@G!c&&B})9m#AT$k1IGcytXp1-oE)%Ni&zpb^>>w8$z=?<6%>yG8)zh57p4Bd*~ zdX;w`p9F;sF7t}E$wA6@KfOGz)7A1YGP{J%@6}}z|0aGY z5uXp_CzoTp<$gYLcJ13<>O48~=WiZ)yWy7aQ+ocN`^w$^EZ<)(w)z*AGK;tBd!9v? zS-C&me8;)ZsN=l$mGu7oJ*>5=7Wa#W>|Po)K#H7<=5#V!zar9`jzXodLd8Dq{@> zvsRn(UN^k(P|vPjmr7_n>A7BVMV&nzlC;UKAA$V3vgws<8pbD+7KfAN`P$sT>-#;h zPQN542M=31G9Z@q=YDag!2FV_Ifx$Q7|7s9B+_C_O0-0%>w@Ep|d#B-_6Z%+OCZz08#V>+LRk?xt$t%nhE=S7qi7v6w-8E#C{=-GZb z3-UrNwj8ykkl?Q{;c{?m0rxz@X4p&NT!AG^7zs(SV~T)eiqLD3@VVcUCrdD%fcQM4 z3l48kZxo(rJRz_z?o1*1g7P_y7Z`8&>B8$g>65Purmi4;LG(F};zxrj(*LK%tLWG8 zDaQo>xTF8S4MH94om~F2`{ttI?SZ|Ly5@0wE_Dw}9RC0}L82rSR4ful7I`&nXtRMB zf*^yl3)x5$i%Qx^Lc%GJ7X|`xVmkN;L|Z68N!whQV(Z=(KabYAwaAuI0^b_nY^uHa z9q#|K<^Sq_6JyJ1^6iT^!~5Dj*KwA^tzEafadTh&{im$%v+~BvbNJ{U_r-%}?o!p8 zLRnUQa>a{F8?OQ-%5CMW7lfo)t)Zy}?W~1IYi6@06K=+$N1+ zWIAHzjyf`H^3!~M#i%sJ-pcwBO=`%rPgs-YO!bQ$9lFJ9hFzz2GxKWq)MjBeg$3Lg z;}92++1YKOcFR)ee(mbRg48zsAIJ%PN;uPq%N!nE%5CQCrfYjYr5 zu}U4fq}yG;Lm^AjU=2y^_K)a1pUOmv(*hFNt#IdLq^Yu3Nprv?=pSPQ1a$ zAOiNpRol`~aR~u{Yu9GKX&7+k36tZ<2ULqoDO7Se^GMcpjk@?XWX!`n-@l8wx0DtO z8gJ>0<&wr$no6V)jAFO#;=eMrqc_Ccckjvp7kA1jB31 zywktL-Bk6gg%$WJHkbIA11dQIb*7i_QrOP-(G(C}#Iu zU|K{eevtL}Ct9K^?D^~5lV`0}+_AE_;r!qCM|JE}?#r(`Ia)4yE*Om&aocb|0~)Mo zu18(0qycxp^%*ek^eGln!z?AHB_V7%!9RH2=nrs%YDm6e(kp@bx-eIX_AHzrHB4@=!(z&_R9m^tDqUCml069J;I!PMFdo4wr#)?|z-?mvZ#jid7u6|GvGNKMNKg5W)yyu+=OcA1?kDa}F#I7q1W{Q8>< zWo|DOV)1_$yj5szF4h-mSExsQ!puZnikMdnNGzfl?PK^004uY6&6L-rEIXfpRbxJl z#PAkf9Vg9)Y3kr!JY!eOFju2UsnV;Y`Z$)Bu^_M}4Wy?byl8n%Di9sfK9cI4^_#cI#Dk9xBsrQ1gL0$Dt%~iL4kN*ShghbHRY0smBN9qu zW{60IgD&*o`_`-$FJ|_=Bba5@8jyi5vn83bub;bVa*0dANQXzBD+RTZh#{Qe+D{7Q z7({Nt_GGLu6pY~v6H+?~J3>clb8ZSa~iuNCE5Ja~qA8N^X-dgsp3Z2B8a5b28zz|f$Gaw=PQY%{#vlQzsWbOo}s%4ahZ@>zWX zL4IFo=$))gMA0>rHsH&)au5y5jWR_qbVrn#<)?R$4y_tCL3+U35nR8>{c!Jmfz>x` z0G--qB4kSivucI!+9r6ef#>XW@vr2ChxusPJ~(iZK~Jf^p)@j2QESXAc#jJ}kXc(L zURzR+nom0FtfrT4JuAfCBwVk~^)P>~2{O56$lASWTzhGg-}%(r)`85gGe3TFqWml^W8jO~5}-O8co6v}E$B?4t2jtB7kS z4)~VHnt*SVBk6y5u{L{wn1gv$hjXeG0kEzWL{;cjn-EZO9X+c_?g8smXeb+*j!fqY z14svHf;?MVET@6O+W@*IY?tKA%ztOD9I|XN$Sv3c=`5dEJEz6_b54eCU0XSx)xSEO zB|$&HCR~z#1kUj&Ap=oQM`Uqits1(GRs;~I+|dZ6UEc+I&;&${t7gp_{g*UhnUDTo zODE;VKo^W=Jm(g+q6Q4XI7oATI>>($xf>Z=9lq^eZl@2d%E6r@FXmzPt(4<7$ND|l zCzpRpHnc|i&ZCi=GXeB(Kp*~`&0iPvTLq2n<6KE<*RXEWpwMBU=s?+jby=a`ZmZkvcww5mUP`Y}+#_)F_V@q?WpTTpr01J2Sx z?p>iZ(EoGIWKr1Iy*3?k08o;=jNpDT9c#_0VA~z>en|f9m4oiYF+y{9lr~H^6o<7b zt|_hW@&>)8nog=zD&s*E9<%{=nJ`h@3QBOz?w*pJ!GkCbk%w|4McIwp(JNDeZrx$j zb{j@N6tTOn5jaP_q8bcE)86u+sznfrvK&vw<%R*vR)*|_Wg`dY^3ZQY{uP_rH;`X! zsv?i{3Rxh-0TN6o*BVmWwua3}+oWD!QE;^c@5A9+-#rh$_A0pmw}7Qu+corX!*h)M5x0MYyB}-E#GA8c7=qcC5UY{eVfJw&A`aT+<_6 zZ*f>_IhWx^`od}%i7Ir-UiP!-9~=;E=z?3&Ul^zhJK@Hr-HSXA&pA&PUE{tNXK%;y zM)9X78P|!O_gIBdC!kr&_@ZL(=N|Yc=|Z>yvv9<9bD#J*W9h>td_|gyE}nHM1KkxN z!jX_jHy)lr5^@cmi92Ehm)o16X+;{`7#wNJom$D9VUOi>hmAMgn+jbJ3t ze>|G45Yg+4Y~G^P02}VQ?<5}x-&Stp?&h3!pKCGtLJKxWahqCxXZ9W(J$7Iu36G|! z$PVqK_QdLe*GyRb;9U-$R{2BE!)SgLh!0*fstP6xZj)@lEpR@uETFHkwh+MpdZoCY zFyUU+C}Hki08U8*ooM1DSC7-Y3v9pHG``n5H>9SB2?bNxytrPnS+H7GL{YZxqk8*@ zgpOTO3TuajW`l98nQ+Ev0F?&L$u*{jle&Z+xbk{`Lif}2~Jetvq4+MqaU>&5c1Bgmla0pc{0f?$D5DKF5Cx8lM z8AXlNGZaKe2FAMI9h9b&m8^&e;7hnR391;F- z#X%1~FbK#+qaNw)3Ot`UNj@@+vi*gv{iWQ%)CR4=Q#)wyM9WddZLrvmoJT76?tELY zZ^r(0?G5(_r%&0i4VX65NV*W*(mT9KXLV!@#jUKVf0;FmVc|uWjK8w8+#Omia2j!( zhb)@4(`CX^we65cT>bILDoXSi#!;urqji)aHihB#&-z16SQeCx^nrwGqJVY(A>M%1 zz8iH+hr1e#F(6%9HJmzba`F)c9XDG}BM?b3-;uH{OhtxKx&l+J%1SQ;5|VwV6tgyN zo@H?yT6y3EU^wLLs^Ial)`kk)0j*fj$xc{J(UfeIZY9@LYf78EdgQa!3aF?uAy9{U zVMJGt7~k5YxeVXdiF2;c=*d#b-19?+ARqgrxscU%pd&39_zwBHVTM0I9>>DKtvFzB zK2*Kf*nY_1P5yb9I2oD6tCOpXRS@4K4D9Y_jzJr1ORaFOzd>3U!sF9LCP> zD1OKuS4F(lmuiGJLLHMBJfs`B3J1vg5bhUY`5i*(%=&OMrW2SLW-Q|((Ez^VqYaK{ zm^sG?8=*)xb}g?v=idf8-CpaZGnK3ZJjVBSs2EyU6?c6E=NuGLeO{_{MhY(svOJr} z5eqLtd`KL}n?i+K4y&I|wx0|v4x%kISi*Bj`M`Zff@Ar@5uuboVVC$h4$+dY=5B=D zXfv$v?|5i8e3I?BHFBIe`7WzGhuSQbtDJ&tvhz8Kk5AnXa?nql^V@*_vQlf0w&DZe zxUji|Fjd_f5tF}s=QcdFs_E=0;rrIg%qE@sUvw z<%1#Kp6ZnO**Iss#QAej@K0UP7Y{irl~C|r174xNyMcoN^a%hRhV`&e-gu>kXBcjB zoRIH8IiTZ?0=?@4`U>n-U!YcY^jKe{p*Io@z<%|k}hYh%!mZ3dDwm1>`XjhH|yU@ex$Y0d$zLpvG2!&$< zZwFzFS*vwP{Ny+6`i$@CkwfaB-at4vz~O61U)D@*+;5ld;(c>85hUMba6f3FceGFkZ7@H!p}x?b(EQ>Exg>)ajY+d>wQo! zI1YGW)V<03_EVD7lY4OXP>*e<{y(+;-2BD$w?@pnxbkoY%pH^$c7E_PpPx{ldf5Kn z1S74j9tIOC|x>0efvOrqrdPV-gsU-K)U|p70mBTR^4;{ zx;XKyRyCedVlh$B5W zCB=+!w_F?l3^e>~9`F@Keqq@K9K2Bwzul?0&jSosIwYZ9fX?fp?_VvhJmmcp-yut< z@~N)KAwp@O_)D_IDQNh=d=d>e!SY`PB$meJ9CE=glNFxMApiYFJ!*OHo&$fl*(*P# zm-%5y`3!XaKf56B6 z0+!l@r?BK_d8uV6F;M}7Ic*sI0e#|O~{ z;FAxW!$l&qw~EUy}2=13Hr| zo|})2`7wInM=Xv9{Y1imw7A#D{W5Mm{kQ7CezOOEq(U(?{E5jAUy&W*#^(UuZy|r; zCnA>G?va%Xlkz9AL%)ZP^AXGmxu2xukYt-OQJ05RKXR4A8GoFu7z zUuVD>#VN#yPMpSg%`tBO98g1l?hsn1b0`k&A6$_vWzt)OPEO-0T5w(m(JxH3FS|J8 z@I79^YR1^T62{!$sF6#g2nu)Y)&>v0cr>&K%6rq}TD3N$$TjC?O2X(@D-%Ch^?vmz z&T%rNC|LzDigYd41tyw{0#ciVa?>JH?FsH_)W>g-Xiz+&qjxN}N7wDxjX|kh#JmV{ zlqQcI3RHTy=r!ez;F_8S+5W?4t~+mzSscGPkOtH~`Qcmh`;SpYpzd3#UB%|`4YMzQ zj!W}LaEHyEgz<=u zl7%MIb@Bg6h-SDGjEMECLp0AfGy!+q)9=&8Tl zMYn(WR^U_f#Gh~>j#N6^Xh3rb*Wg0elY3o_1M0d*pZPob~^d|_kJepZtr2`;7%Kys5XJ+F#>nAnSt?bu=JR>nIPF#BoEPj9;<(~gfEV< zM|ZexgbRC5M$k)tm>T>`%*sBP-a}7FEja^?9#_sfw9Cb-pBOnMlcUYP7W8g;t9epV zJU`E)uHGsx(@1$O>tA^F&#o1l>=y>ic5F6Ky?oCWxm%*tR#VWX|LL_RaiZ>gRWToi2G%k+wJ*jeM8!J8;Z{#+ zO`l)Exbc}9@w|_-22Oph+Yf^dSDd$bcO7SS=%lH&vyS{=A6L{_Q-+k%l6)x*NpC3o@XD-=iJJEkg>fOqjUG%GUrykQ4*{{)0 zT=-?Mi|^Z?^?cqrIL*dq30tVdPgQPv_*d_-=iCJE z>|QQ3gL%^bAYkeUSMhL)c}ZuR`@J-6D2&1*mSil}uOPrhju z{L-CI(iiRK!@e{uWONrj6r^p*oEjJ$T468rfD6V359a>8TyeQDPgG@pXX7wSb&n0# zW%SzK`E2Bl|7p3jF}hk!>0N977Pi`Ex97s{2lhs~6Xb5TR7)veRkZXPK8C{X*2=!y z`MLtdnm*9|5!FUVidvQqUULm;+v?X-zuQr;>*uhRim-I_VFv#5Qq;xM?RPV!VeMR0 zaiJ~OWmm)RUb(A%kV{a!TU6oC8cO?NxOlUKd%KQb^)Rzq(gaP^tp=Ob$|{a=k}5aT z*ySZtRjAB1@Q7Q79fpFtrR}tK3z^gR*`M|yEZ(9fzhtvj@1|8Y)}&BFd<*P5NnD2Q z`H|zj$!|IBC0S_he;oSdJ4DE<7;lIA607?Y?l~Hurult~y_uzUu}q)P=QP#6U~i+c z__U}>;^V{a zr(9EOp(-GM%g;KZ&QABzz!rbiMJ~%}%|YYh*$~l^*y^CGb3Q76&L>F)T}Dy&N#gWR zsAh1c>+3%{R!n7Awj`~M=(@<0c(K);*kK2MC73-yH>4r&L7Vu>k*i1_S0x^^HAB-4 zll8UMA!nF^0FO9AFL^hXA?pFS*ov_ktl8?0D#`!g>YSo80fKEE+qP~0v2EM7Z9AFR zwr$(C?PP+9lSy*tp0n1`L%;V+*Q&1C``i3`*V0ADUuH=7cci%&cDO zdY)gNMaOPOP~$(ZnnSF}#!%zEL9vc_q*?z?^bO`OiKMq_WMSxWKS`(uL;8%F(O04& zP^kMKymxyI2(gUV>Qm&D{Qm;Yh+3k+y)aCFsKmITyyTfd21~*#QP20=NJ!Wx^R7 zAgc(RWCEu#qR<6yHbU4z@EF2xz*!p@zNg{^`Sp+9g4YAZIGBIv_D0(Sk9$aXFK|zO z6TI6Hu~zW@gxM0fR@7?6=S^ZKh`U75LTx9$y(Hhl%9o9?%+Lb)Tg+z}_G0vj?n7oP zXDw*lken_8_mH9kA#G5&1EMZ$e{gjH2q)O*0EH7$T98H(^pXhFG*~wQTsn_q<=8sk3hH1wsr&9TK)UGUb#aI1p@V>nXbkeS*&F!`5d;GZV zv=D#2IC%mRZk^I}ep*!8rXv~g3q*fz&v20VCON#^~fvD*lv0=tVxLDXGbE8gde#bMH@)bg|)EFSsN%pD`9af*)ik~myM7aPtW)Gp>uZ!zao zJAH^jC!V*k+uss-#u~(-PWABNYU-&Z27XJthXzOV=vv zWx=Bu|ObP`?wuC+sj#or$`Huzu@4>sM2&ecrfDRXB*8MVtvoh7&QO z&Kq4i(Xc_p2UP^7Y9=e}DO+@Gk~C9&oJ6x)%~>Um62CAsZLEpBeXugh!Rg*OiK4?< z2rbWw{XDo>3mm52@2eFTymVHM+iT34r>=6X%v4n-4b=8|dVP~`{~Y~$Mm1^08!r_8 z_H}x@_sD<1bD{;L55LYK;(B|~SEDBx({x>9Y?-vi-=!3i6MGby-AxPT&!Be&Br2ur zZEViMY577g27Az^ZB@=f7835py`aRFp$${CFb|Ut*4o!VcrCtSZY_(dsZBYlXn_mg zLT4TE0}#T5E84L)N2TzJ0ysCF_EGD#DSCIQ+0U3w*(I_{ngxW+OWYY$ zvI`6mXb_os3VH@z!9^HtYfv0}f-!;W zy`S@URGtG|fc?!w<6%6l!_VjS&)YDr+4rDlh{#WPWWWukv#_Yq@4QLfVGrvdJ5DopQ;-@KE%>2$?WCpjNzekA% zx__TfE>m%F*Ym7C&uYKxcRsZDPvAewPt(q~+b3S!yPjVfmqn(BYVUrUp6@6F9*Z9W z{QfQuHdlwW6IyxNJj?eSKB}-b3?7@O{eBdycW|ng2qF5)n`_7@Z|-~Xl*JT9MHy5$j`Kljh^{Mu65ZrkY8rDD1cW{kLZ-gG2{i=O*=UR|I6 z!R}rCXjpc+&i&xAn2S}w@@j~x6PW+|Wj?tkS3DDVg@>}ct=iS2N@rg4I8(T#+jJP+ z80l#z55O9_jG3s%-5gSupC)cHJz?v}vFZeeYtuzojh&Un3!w6|K3Y5>slru-fJhx2 zEPNWcA2>K1iBER){%h)eG_p7;rpi;l?JTU)H2g|yp({U~r3RrK14a1}fi<-4t)x?t zCo=IPG?s2PEtx4uakNC6Eu*gfFQ4%tlO=R?x~;8dbXuL`RzLdI`sccA6A^Qiwc&BG zvuPH`8}9{p+U4T*j`$A$NVp{XDFI7q0@O4zHi1->yx25WHi?==PCg0SIg3}EfKnlW zF|k}SYbJ@SDAB7)5Y9LWg-gb^Nw!Z~ed5s+$nD!e|L6Ze(vHTK3GM$u(pvsO(*8SO z+1bqYe=ySAH2iFFPq76)dr$V(CrdH&vs=jR>fN@=-1u`gtZsbSFuUWk+ggIy&}raa zjSZA3=bd{6%-?zK{d$DI?eEJyR_8^Ig4g9yoRM?MdGwDz{?Nes$S@~^ z<4dMOU(O7s)oUP7wkn=-*D^N}W~4|OSN7!1U2NWJnNv7h(hA}?TQYemh*B13eJfI0 zq8)3M6*3ST2U2HFpI+#$Lw^$G=E)NYLj?zeNCG&&%XTT!shT@?Rx0O}dkV9p)2FbA zS=d(z<4$9XAr8O8gbxr@rRQk#8&v3d7jQ=PsPigU;$qYD96r6@H@Z@!uEeG+FBm!U z7n46{HM**^%9BNRf|WLAAhA3UA z^>WtA3Jf`DK~lKkwXC0xz38*07r-uc*T7e_es*R=bmn@77EOtrS`uor=1Qfped^Q| z#m(m7=?%neuPrCZa2;OSTif4s43)%#V8)?;1=2XZ%!6%$9yv*towz!j;7PMRU<{&mgqkZC$* z>OGk~+?`-HA#m@Smxm!r6Bs;f#8TsJRp~zzZ zH!>U(bTJ<*`+>Mqgm#PyNYG$p?{A>Fez22>!KS&Bo+r=*?=>=Vq?iz1d#_&69BQGs4j zSOZXY1nH@~i)c*ci(tfVfMpf9Fm-K`wXBQ*v(UCzRJe58uAMbKZ`1RqAMJ zXR93~)e|O1slm>gSwUupqKzGdgJNRNvFS?m33VZ6#(Iy$POa?MLQbvgIQ(?g1QYs* z!$yHuPgT6!V#U|sKeCd#poU!72zHtRP4^cLYcy`i%nKbKyFa%e$y9(~PQ2_%?ns=UG z%8%>Xvl<2O(q50~U0qn!ZozB{u!mOZpGFPFE~3l?H4nTcS`HrGAyMd(3s6^M*7C|W zX4(hfP#Z*V1_&|wlFOFm4^eS{z+=!&ZA@b=h@6XOJCMf))`qfWZ@?$$fjT$OYPzxt zc50EfEK|GDJF_e)_H1+jT9s{rRd%Q7cFB4=M*SI{tZ5iPcCZ;l^bWIU82TuHa4|x- zKdFd+CNZhlY;|@_2RVd9AOFlfN6r2*gLSKp+XBN7jGGU(%C_6GHqNiL19fhpAS5eU z-hiuk>Kq-JRV+pn)-JT8um|SOmfN9`B-CD~b#zlRgeLxErfMT*_lMB>vSD??r9-{Q z+#hkw>D&S;?+s_@aPO{RAlg;aS5MbVn@04+S6olmD-8#7Wv6rhvuhlwLBbn*c$txK z^XGE}Vat}W=##)B5V_F2BM@Fa7<2Fsz)4ZjvDN@?c#%5%dX<3}V6BUCVWs@76cdkq@fy0^Lnz^* ztQfoH@PY|k2mNoMqx>4ydx9D^u75))k%`55nQ6fnNOLmcb*zsZR>9mbh#os4j2D?T`Uy4S!iXl=L`3-zNnklYHM5#nl~(I>a(dU6774Gf{;8{8aMKS@ z9?)j;bA*D|G6OrpJc_I}?9y;IM6VCMQsWfgfjPltauPVX4)RTGzyjZ?XazeQB?dn@ zMUEr-iByR7X4Du?1nooChX+DLH!qUMNrO=1^5?n@O7f%{2#yWDVdE0~9TJABvO96| zBGs`M1ZhWmQa+yhTk)O-at}Oba2b&WyEOl7utnn-d1GNzU;BpgEu3?U+Thz1h z&qUtou85m3-8dqk!_XcNfDs8cR1wQ_$#gkG}anSX1Vx$|r|;xwng!EbKD1IqmXc z0PFf7QwUKmoCQy0{;K4$2wnJ*Yx9Sdu7egm&(wY8Z7hDd-?6?K--XFH=kg^Z#FF~h z=(y{Y1MISuU7YOxMCjGE&>Az|R{aMHgPUx<$&sPoE0YiXa{5_S^kwKfevLTgu*a<& z<|R%bmz(eFn}|WSpOr$(u8WiD_Xl4));nhYo}bF_oC1ciSmj}F{+`FEWY60pCa%ES zqvcK8-PifMM;@Ol8DYR9tjYe4A0kI??Pb7aVRA?StnaP=Q|{u`Tt46T8tOVU;?AZ| zMdiy={;l?#=lFHzSowO}&hsQ`evk8A&nK_@wasuhRo?EY(dE;c z+kl7Dz4_z5beph>B`;2{>x%#FW(v?jNap4xyI~QH!Ck~<4$NoDk=CS|pr-g(6 z$9%WK*Xeui@j2h!vrM7P`ME(a%6moId+l6258(S6(IMaa@FCOS=6y`<95>+0o3tH} z&O5M%@LNORdJ{zI$ToO2vb~Z#w$Ora$q|2~OE92%kzVzXDHcK?YNLY4!G02%P$FVAY3I z#;G51M{Mm>QqD=7D`zfE%{NLPkb)<4AXCbzVd`pmH#ZVgXk((X6O9ny6VYExnqrCA z?g17oBlKqIO2*vc;P`{4!Jf!KBea_j=kY;FFMpH&gBB57OKg1@b7}P@d5UHG_{vtk zRxqjEde(Y@?o3Wvso+#=R}7A|rcO1~F0V5v*`{T{G`;ml!c*mBhIz_L5}ejE_wYIZ5oyDwROCC>0%>49y}{ zHo?Xv9iNDLqVAluBP5@Am#pQSwIks#NI*pKj_j2&lPEE1ax#7*^#|=8I+Jof5p#m} z1kWp^N6KHsUt*8sF2Q5+^8~c+`{(|=;|Jt_b32ytKgR#Ri8J9JwBx_Mod5b$R|f~% z|L}6!X~DXpsb$s4yDh!CD@(amfr6n>U5qKZ66BVEvLSUyl`n${qey~83YHV5mPqy2 z+#;CbXUYyR*VPOUacUn|R1`NKn_zl~>&|mB@N{(5aYK#^=^2?nFwb0Iqc5&{bu|6d zR)zA{ah>ONJ@5Z~zkc@rC;_A_?$*S^wU~Bq9kzGU)rGY>ZPoE+%uF6+op{lsAxJAv9Bo5!6PpKTL@6?6OOKmIH?)%`!_g9^r#foT zO2%N!#H6Ujh6^=yysn$|5o_FD%hx!!eY!;%8XLM;vispC>6{b4)Lye6jJ_s#h_g zD1?bT428lC$h2lC)8Y);D(_j8p~7BRI6u_D*NhegD$9f@a_1Ti24LvVU>_{QBo*}J z=jm~aWD)qmv(;oZOUt(b{etGoq*r&I#CD$kQ5+$Ef1{Ub>S#PCglVoC$*r>4EV2&DuG}>=nShy^{FeR=WTp`xNmRZ^0PvZwm zvJ%l4l4!WQgik@%)(~@bAQxU7#$Z>x$@r|-uoP1zIuW~U(o3RQawfFIm?uMYDrYeS zd4=*IS1ugcQmZgwjx^xW2k2f-OXHL|f|A}eJva4$^+T}skM1wKukY`sOgKV#@?s|~ zYWb3)PHa7C_dOr8U5yLt`xW@}B)8ixF}EK^8Jw)EBjj@e4-wLu`O0Yp4b7!?X8sDb zaB6rF#3Sr4iLk^_BftZVfnuL1TU4niS#WX#ff^gDMO1Ptms)$61=O`_RM@#!B4y34 z1Eb+8KP;oSnS1L@^lv)Smgb&ea#)oS1|&#W>8k{-6O206cBEf|IS9rUcJA5BXG+WF z01Xr8dVtT@&DCJfZpe_*dcpDcT2534#Ac;4Nyf*4pn%Q+5wJLsgqGXVk$W=Ge3isM z)`^wm@9lxCrL2<*T=-~t=Iw%bPrDqIk zFaiBX0owr}*lCdlUJ0E((XtI^aVBDYF_%G8bO+%!Wq<;-*yDktA&jNEQfZjL;pxqd zHI^W>1I1bxQJ%x00y2hdSu5rPTtL(vT3Dc))4(kzOsM=KasvZ&#utK6J`V`pj8?YR zwoeczI68fxpfazUA1OJNu3U6KXin&*NuAlJYhqq9!06cU@_0!B_FU=E&V@JA?-8yuuH zE6@k!Km(z779KYBbP~^q_mtI}; z4t=*(H-H9t4RS|S=Lr=cAX>*z_|fa=g$G8nKXdGqGlwKd!O#nstS>r^^c$o{HE1yI zz_2x1*Rcr`Pxs=qDr}W$#68HV1=hDYrrKRoBVF*Q>7R1W+|Ukwqhhs~2qa~cZn3a~ z7$ye%8i(F@OUeytEhG90_>~v2J+icNOr(lFYjzm5V6`9w)d&C|U)b!aevWq0;g(BI zV+~eQ8;Jf_f66-SsK9mUI4kg7n(-V=Q9hRt^E@=T9ffb0*tzw?wlZA@1UD-NcF`Ai z8Y3!aIJ8bF5i+)2gw0wyDw~s+#v1qnwnvvfqh_|eCrw#jhT4E*REnY{7L)ok@C9x| z57;saT54n%1@h!Zx;Iv}1tzHoxrF9V@F9&fja=|5kv@2fRVGL(Fduv^aCmj^jx%Q` zvA{fTXmPWJUSO{c{ZXR&oSE>nQCuzyak_cGTJ;+DhMtETprykKu=37r=_rJ$F)zQk z6?g`^vKHLThJU5nl6U6ESFvaTDp=^;9u3v73|$q+3cU#-FF40GOillerBk5bOpkC4 zfzkn{wg~RIWu?~9LBRpUZU+(c%bl%x4T8G@8?!w)*ZP!~Vr3*Ic3)P9rU`m^VXLr~ zeGVh;{l&%V%O z*Pki$Ey@EnbT=(1ZBasAl-9ZMDWi4qjn<9P+VD07)(M{E@@k`~ybf`(?1K^g$j>b` zUBvCNkB{CR^L=f?cCXu#iPkPI;f^@J9ST%yyI1B!cfx!pmuylSk^;dRza8#t4EtW_ z+T#ccp54B{R>lNqv^T#mTVz*8RPFiaL@BD8GlU7*5BQt*PgY>Ex4*Oe1MBdP>6WuC zxCz`XZ?+Jb%cFYLqh48*G*`frOPdN9!3u?LJTjvL3}0X{Bdp7egUBo)&AXHYIiuWG zyMwJ-^{2jTxjBT6kg^rEWPV^vYx1E8NsIjTvro8kACMD+717?ni|i#L3l<2qXb0D^ z);&8!C-UEvX}F=mal*q9Q$e{p<{tK$&(_tqs~q=LB1B4S5Z+%ORRyIOep)xDTZ2z8 zzRTN%u77w)tAI{{D^^>eTZdUnr55UdX>dZ<+;U^K2j%r<$GC=CHp)@k;!w(7-CS7F z@W^ZCLw(6b20*zcgdHWvq_X%%7KK8Ckm*5rz-qVJL3_+ntltDL!5i?c^^_Om9nk(* za?x6{BGb34lor@UasDSh5O9ckKk`=~pnLkU34mV#mmW-$>Wthc=(zA4D zHhlvItDu6b>)6@BaEG6^3kt42<S;!V;e-SCWpbqJj|BL+Np%0>Ex4oMawe${wRFsBSttx8 zh)goka=uT9KG9k~*%ZcEgRR6ITFfSl*B7e(MOUpCC0@a=;28i!ANlcjEwVlnFH25H zTbOj=<;oITsY(19%Ohm5&$rl5!^C#oSQmVXIr0G+^(x#f@&W01e&`QzTGfK425ei+ z?}e6(wd3Abn;sR=?bnBWOXl;au|dT7VBMmvr`|i} z1GOCuSoNlFgWujwc?4iOULl$Dqd^l@4vzlj;D_!uBxLWvRljig6`f?r3E_rA2b>T7 z22mK6FysE)K6lBulDQ=^Yh@^<4D*6`D~~<+%UtMM+^V)yq!l8e0+4v!x+_TPO4mVR z(+$r5w}I-n#k-JDz0d5wXb;s^j;F)&9<&zIzW95DoDxA~8eWfy~W4~0+wvPGY z$3BngN7v3~sMPs-^|_unT>wm1%m)%2f*0-X<$v39oT!#l{>rcQ;(x()#u*k8k#}o5 zoo&bAUYTgHe&FMN1L}L0WVEdE7;mQsUN7P~6*%GLu zrh1GwiO*#t-u(#G1$fl=F6#3c)^l!9+h_P&Ki$9DDD3xGP9HW|5RSJ04fFb38zZcH zFZh1RW2dYx{_U{y(0{kX%HI=nr|-1hiOc`T-~XFOPhx-9c|~i@>k;<+F7xrP+|R=k z_<1q?@p#|Yo!ePrpxNhl*%a8Ggaf!~>QsmaLN{>RQa zJtGFgrd`0}EMfo0(C_bsV)i=F_Ma)7x1^`s7lE#uZpK|NiSx$PevBLi-zC3&3%O-N z|7%3PasQY3eIN7VDcJM=ujGD+aYpxlOyoQMH<$ebp8!t4pBRU}*WoaWxgA#)b~WYp zpUKK|4MtaRncKc)clqr~-i{*pfDVh>QCkMPO;TjGll(YZB_ zAd}+p@9zsnP?ge)JVfLAXGX6mRsPrG&$(K}{`$V}6>d{ZT-~3gpN63YOxykC__u=rv?hWMk{Su!)liT5N zWqgqufe88iG%}vg`+6R1kf+D&bUGQ-ns~Rn!{N35xO&&eX6(gr-TyFh&2wia;G>b* zZoe#o`fK;+*Dpfv*Rt^d?QV}=QvIigPSJy?#t+bO{eYXL5R2V_=GOPFQOdIUpU#JP z*#0g+OR}i~2IQ#0&g;tN0w$pM5c)NL$7T8ww}voIN{v#vJkN_$+r9B;?v73N=d2g> zj#2&(2#dqj68;sZRm^8WoV47^ z;fn1nj%h2dw?&}^#|NLOXUvy47URqsu?}$30?@#A-WsQkRX*Bk(gM$$MzCJx)kZSg zc#u{0k@_=}U29>mOWNac+8%J#ttA#M0q-q=;$DF3gf-*05O6Br=_gYL>BUT*S>eo=+P0G2@ zx+yT0!rqCSy(1@A9UQkeR%SjQi?Vfgjp+XD`V!APO4?X+k+_TO5aUxHKKTYYhBfSc zrJ)qXg)1{rEVgh=cP;{6Se1Z81I@+enW-utHDMxaH*5&e#%|j5r(-8Ui3EPDAGL1I z{5!{hk=AJoZhI(`j=T}K0hIDFvs&N>$b^u*5sz_r+6mu&K+Y5V<>)!%K>cRezX#ak zFwPV8b_~=L$zh!5!5(jvg&WEIz2=DLp6^Zn zFOol!Ks4dFz^KAO`9uGa${XN~%^ySH*AHrctifRYA>dx?U)&Y$4?%y#{&>I$;gQ09 z`5WtBu09fU$r{r%yd)KE8a>igNw*|2`gEGocjZ(vY5Phg>m+q)0%#faN==$%+a>QP zNxLM4Eu!~Qf=Z36WwY2YGOY>$BA7g*yUrg>I`!ffSz4CueKK+=>EmjrQuK}A*m zmGZ&CM`eU^`rV;p(D3x0<1=^uh;Lw!AC+7R2V`VdlaN2VZ4L``4?8TFlT;-5AEGg_Pl{S8R%Uho%E>~a7-S{4eL)=n{ z^_=0h)+AM3gUg;=45l=G0N-4?ab>*GKh6z~r{kb)RBElvQN_+uc>R z`(kR6S6U*47Biw8q7=S7dT=3-rCV z>5tjQCmzNuN{1n=+ZDJp=?SGEJz@8l#QTA)`+!CJ3%e6!s`R1crm@KBr0#(w+=eqt z`s0ck4RKWEwu>=;Xp#TK{`RUoX~yD1M>Zhl(!tgsAf2>3o#XM_OKy@y|&<5<1G43hB$ z;-7~BNFhG76JC3X)*BaegG+?M1Fc%ef<6T6ql9dpv6l~;{VX`V;SPj~k+6*ewE>U3@T(K46w zy851<_1p_?KnMTB(}$>n*YDKj9ktsCP>D7D{ytq_*MBeW?Yt$%+d^RPW@idoi)M-G z%tToZK#$Cow~22bFFU}g3E0?eH!VD>CccM=@tqGmq!@VN{y>QlrRf<%D^_*0@m=aL zr97o%skY?ng>Jc!#tO*mB4v~S*O=8j6HZGN|Ihdss>7Wrt|_TiRIgK?_2%AThS3)* zD`cK7=wN0L^kIXeC>C0HXzYE`eGwDgNoabQiLm~_kdqPG&5r>%&^PG+o+~FXT0Qgp zs}Z9AM2r7DS9WnTHnnp8&ty0|H6>X*F|54a&8BV-I=FTTL`%DNf?74Ov=Ew&Ny8Y1`X;9AVBHBKow1LWsmN2 zm2#MF+t3ZI3tT_S#I&mq#c@E%90T&B^iU^IUCZAZLfQOuvP%>*?BOmnG1*;#cpx=RS= zNA#h2t*%W=)Nz(Gsku&7X4mE!R$8|-#yk6o0w}?L?nv8hH)(Ufk6bBv;)FkGah?m^f|08wfuQuto<$8G zAE?}wpz!`EQhl^cRl?H@F`ZR{0AalxNr6Y8a6&ja%q)Y&ZFzD(HUz514G2jdy?qp} zHs`FRpE*GcR8%=)RTAMBPqRpBDFf?A?1dIs^5y3 zCQS8$>!<1@hKs9_quNgi*1S#6L88sv6Yv*0s?~erIm7NbQ{lvu*mn{T=P04$6tt66 zpsu{bMUsT+JlHFXu^!p4fu7VtU;G`w_k2T8RY9Vnjzq!tK zI9~Da-EOBCPZbi2Uw%x;);P`0+1Mc-M`aEb%0e7;=Jj|=Zfre?u1 znoTFmFeOP~vy(pJ+yN`j8j7Od1DRD-<7#X*un&2z;)b)TgBgMDzksc|SOKkXaF-fM zLxPD{7PBG{cE>Q*$=?A%hbq(k{O5wUEuGq{R|%yi0w;Vj$G@HqM5QK!i5D)jgnAn* zNEbna5u?MpD~W{sP>*h-9Mj3#ja4vsGU3E}vxRPRU!tp;htWz`(==e{Qy*C<^IU{r!drh!@~& zcwILcF6aMl`E74mi?H+kr+78$M$E4u#>VsE`XXBd!0^|LwTw63|LyxDYQJtrA>d~S zaQm@Pnor>G%z&Gjb!UK+%?Aju|9&_jWUbftx#y6Q+uv&6U%2HT=x&|x0DN7(f+i8;b()~ub{bzTBz|OC=>%vSx zq8UHWU3ZPjgw6c}2XY&++6R@^Se2UyKP4|UFS)CPfSUSQI%Em?vW1NrUP^+T&a6~v z;cq_=&^_Y+F14(@SF`p1YU~N#|CE@yn%Ozp8o8P||3^a>_HT)YI=;HMI<;1FpOLDO zksgO>N)VIS+h*1P7ycj;88HZv{VI}H+DfmJhQ~KUO2&-!2UIE&HL2w@nL-gkKuTJ9 zt4CVWr$T4KDv3f+Urs?@B>8MHhish8?y+4e>u1h7P7Hyse-8U@H=eni!i`>VItvV@hk6g5(O zO9X$dBkSHRwAk+Z9!ms(0$;UxsK|SWmx(;DYGM>nGOVg<3M0n4UED|zFogDdPIh(z zs>*H>f!~Y+@ApB$gWtF2#6E`lG4_h;&Qk5IC8AjHnIToe0m)Sx96LtL*jNb#IclLJ z@9;iG9Qm6mSw(Oe&Wz=2#Br5ZwgAQ;sidzVhyCx4@XZ|pm}(3t@;o@ow-?Qpafw~C zr;0{|)vY=TkX-(IymqppO5gzkMLTA80{GX#c(IzUy4%6Jm_^ZY?ktFh8XJa? z!i|j%9Cgs*J;iK+y6mH2rGP1zno?pdpP+lR-zG`DgNfu53zf6-ji;;=VhQAZo&$uF zvZCFqk~=$~WX{$tyLhqUR1tnO8%LFC_Nxu>R$C^!7_dQBe(aa|juo>w(VxcF5-c~r zv<5X<%0d8?#!_mE?bSi1!o3(Ekfhi_b9t5vc#$u+4I>^;p(2U5%t7)K+N+RXRp6?Y z3BokT70IewnV`LWnq^(N=2q&z`gNYMo9TZW;&c#zE+-e@N4H3fFRjS<#FJRsG3i%8Aul)vl0<)-M zkeP~&=RJQug##^C&*V0YFdF+NctH%3@?*0EU7%K!3NsUNZW!f3EPdl_yKY7YFIL`olyw5@kvNpaCK{%p?1`rTrGr#q$J z%W&~>#f94w#21uR^SY9aH&PvW!YAKtsf6|$hU%;wElRTP#)P2YD3PJ9x6`e-ADm-QT`m+h9LG-yJWBEF~^PMt^S+GCq5~DOefSI&<1R}O$7tK zJ@QV{7Du_Z0|@6gWQ-?baVeU$ZANvV-N**27??vpV)9_B3=2tJUl5I9lDy%jcBQte zK&78ja>r5zRmYHPf+{z{10yoDk46NlDxAbP%n<}bEFVWudcKMQ`-%qP#+d>I&yb=& z8`R~&T2cIdwRkQ!mn&?B5jz6u46IAPO_oNz098jY2ZkhgoEQ;WRIgV^y->YVI;ch3 zD+6&D(U*t853MF;NX*D0ZirRiSs5V_!qp#YoPdyJ2(dso0rCqjw<^QCc8wByk=1lbZJK{@FM=QnN!x5Vm*yp*_84s49CMbBvFNfLaUUo1`Xb4gvOz1 zq0&$mwGey)i5W|DWP&AG5LKe#-;zqcnw1(vK@A?r}LC@j-^0Z%8mY z>NS3X6PcIY67ic8MnM_46@{NPG)5RMl2WX~_olvzIsp?lR}q#S16fWI6~h4E-0@^7 zSv+}~$)`hDtZGafsNpi?frOKXaFf=`3(q+yM(g_1j3o|0m;nF6#&I5A0dLS|rERn|or0T4!A78tL6XG4B#X%- z(PxtEV=cj~fdo{d7o94!!2DFlOCrnJ?I0YU|qElus%IVghb zpu(-i0Z(5wgRT{IO)yVfaVo)cOnjQ63WH-nMGI>(o_*#XQ0av2j{V~Z9o?Xbav8a( zQdo%Nr~_($u72dDn?|zgs7q{?W6#t&@HDc*QQH5cZ;;}4S$7_M?Zu#Lq*=G3$fwB= zsQ=2VJF3LDQ75Nc7L3_YQ3aymtObYl0Hl>%7iT^|RWjxaPhZxIjzK?>hQ=uj1)F&m zW}JO9=C%WZupUQ=Qm1||jG^{RS(j296SZ0cEH7$Ts8+}lR$|PCt5SDe>uBsb9vnee zA&yR&sbreOFq3xZIcK#xq8kjF3Xx)&?Ih4oDTrAz)!UoxAd6Ucx(G)o5w21^SrG9tcx~i2eM4+?^4}AQbmN({vHVZ`%Mk1YFD$9H1JSUKXC7RV(+j z;+h$o3?~OPn(EkFF*dE)^7PRd#k2-xLZ=NO5ml?$z6JJ9)cN2XX>I1MO1m6ykIC1y zNVcF4u7ioxjW^aw+$)G{Xv$I^LkmP2=SZ$unh3DAWEcGhj&80*fh+_9aRMNKVSqUX zGAz-6*BdQXZ2_?=;vyl?axyUm1R`kJdA7Q-+nX<1qnxuG+8QglsNX4dECjamCgPn* zHam?WMZ}EgKw+*ZvWBvx@&{4^?1Lh^mPxPVW2h=5QV@|_1x0)5tb?5FBhk`OF;|4~ zDk(P8p*?qX$vkSB^C>dFM!bKwuEiyfZ>332Cy5zb4oXYOSfRG zcaUOy0cWUe+Hta6{>3#IA)%lbI>5KCfEUpC7gxOBfSHuaz@79dG=U4nS37{b?E=(+ zdSyOadqS7@R7P=ve$%nJLg3!;BHJ;^o^*Wz!`G6gfrP^_J!!i5#A^X8V5o2ZYRJ0z zK@OtQ5q>3skSZcZ30GIaAsDh}mWhE5!5Y7=S=~DnGNEVyco89J$*veQvV2}%67&O3 z$lQIn8CE*H&qE|?*+SFA&|?#X)dxy9Fe4QtYqChP3UjW4N>9p9P`Fuh1s2M11d zc7zoHEDcYY71urD8c&jvkmLUPs&atVm?@(NY0R)AqcFs|lRl6}XiHAxlr)Vpkb+I8 zIuIMWkiwl*?m5`O%z8QG#NDhQB9yTS!P>5Nl-ocVjR4pqRu>9!D3h+87|>|8!EDDy z_*zguOvup-ZFgBan0Tg#%4>`BbT0GSlGlJ3GtAxx22bpgy>&h0zxp_C@-8DPQkB8!=K$L1v+uAgfTZUk|ZPVI1>GKMJAqQ5%G3m z9P76P6TudkNdDFg;DmYsrl2vrFNH!uu9=m-cHd|MZ9cZ;J2Siw&aUk+VvK3}UW^$H zm)o^22}7gPs2pTVSETHRw3f}3d)@A0`TL6PZ_E&JDW*V5ZRLk43=+fIUp zpiU+Q0EeY@KjEyMW%h2X<=ZX%+uIU%$&~|}<++_RFQ%+lud~N@Q}UU{bz8vR<7(#Mc`oEm8SJ~mzM9)Dc$s1I`*}5gf7eCM>zTh6 zKGhz#h5qWwt)H4*yXXrCzZXq{CeNn%5?RBZ)2zZ(MJp1pVm(E_n==hwDGlh4<7>iJkBtFFW0XQ z`7W2EG6LPjAFD-1>-7ZPT~$Z-0r|R*`jdtx?wckNFbpHK3TXYkH`%d=$%sF7*LHUf zea9ZZxn6~y8x43jJnm7BU41QSzgS$D`y79^@=ZqsEIsG_OdqiBt}Nz#yB=Eyi+IxU zJ*Lb1#=14XCo9|cK6kFBdG^|O-=7iNYrGzZKVJyBueu%?o9lDl-4AY?1N^Tx&XVFj zAC@iL1=zdK0&MbIb6e-PU6&0UHnV&jJA2x!hT~}-N>7OE2qlm)R7&9EJX~C$Q@O_f_%yzONr2D$kzU(iY1=cj@V~sITgE-hHR* zeEJ~QB|tNSa3lHCL1B5Ck`M8qTEhi4-q+)<4rabl<@9|0F894+Y4?3mr97_%@9!1l zcsg%+ZB6Ijk?%r*|Hs!mM&}X*+oIXAogLe@ZQHhOCp$YfcWm3XZQHh!FMK)ojQid= z=iYb5=;|N+t5)^uTB}#hniE0P=i!652XNpVOs)^83602L%IPq@U6=~>%h083`FQLS zXWzcNVDE0Dxqkh)O=VAa$D^f7(tiaU_Nc<@WX(U}wj3VV+xc4$M|y_F@Md>@Mv^Cg z8?A-Fuy?soKKRrhwiug#-X9kP#y@=rzNp%_I~bh{uVH`PCaSAnzgIr)QH^yT~eSyhd#ikITj zC$2k}b^gKJ@<01hGhW2o0tqKf5z@986&=Q*TKH@pFP#)BusaLAUNQ}#!F)IiUB1Q< z&Q?j37(DG}L@eIsmK6-Sh!lTJ3G4l^p5^p8JaS!&$LAU3+tV<*I4{@6`h}+JF3{YP^EE?1`q3nonImW<Q^bhUp-;uK5#uj_Pf{sGC$Uy?43#bJzpQ}ouN>G9$(+hVar1YfcN9-zOK_2J{@6&;ZBs{X1Dkp3|JlKry%!u^W< z2>lTMApc-7kJb>) zPr>p3t0lNPm^&GonEtQhEn5u)7v&{P|1s$d?*%@OGwFcl#!fcM`C|WhGEpT8-$uy= z;GyZ??1B;thfQ#@4^Db|&^9P(4;0EJu%f=ojd-Fnh-gD&sT(Hnjwmt9vb=UoCZfY% zt!uch&KV~*JH9tXa8@&IHyt+}JD*!OGdJ7s>D3k0cKM}X)iJy2+pG~Ra*D_9lo47J zqx%XX@a9UyP9$XY#70U<#1R_y6vr0>^{J5NNghPHadAX-4k}XcBjOR_BBYffCPYd| znle;-8aml21;H!Gwkp5L_m#+G9R?+|K%Grd;YLu5n3(9ankDZ{x+%zs#*i8|Y{R7( zkm@W*3I#+1OsC3Pp?c6TZ^&fVIo8RkfD-KGL@W;gq5sMKt~B>BG!0eCybt*g=SfBQHd!* zEqo#jZzhyf>K}}3tPW(DQd->iB=F!VRw_z}FeTRaS73z42tADyLD??V5=9bO zcZwrJZBfc(Yi%F*1!;IZv+B!82|(&f>nt>S4#bOS(O{$)VGh{w-Eo|BHGVxvv#_lo zm?u1Cd%^03DT*ez6Dfi0C!y^V&ghrw0x4c~L9$WY1``$BE@d7jFl zHg>0lx7ICFgg8oNgF09)?>CL7&R|omO`_gDZs{^+HMXPaSvIxQ;z)|=Lq+ASMo9(o zy|t3;KPS0fc-fS$;MpCjpp^A%o(p=k;=hpT>b2Ll5G@>fFHwb1e*~(#t^+|?1S(2L z$K~GqC0f1|UJ#u2KzL=;kRZ}XEnx+)EJryMA%U-y;s<(NOc`-OA58y3c@oqPseOfg z)3!g+!Y#wp!YB9(JpYx489fzkf7-vyPyXA90)j(}DDWEy9l2K?2=OuCm+p+Sf@EV@ z6hkE&%TevAFrFR4MoB)h?PRsQGo~@HIYf$dQyvk95m@HlE_E0t+P?sS%81`6mgOBxPj{Cbpa_?l~puY!Ibvl zDK)BKYe<2EH~2XTLbRUdskj9HBfX;sF?45!Ikgm$P=Sq!gQfX{M1)>m3eKcBmN82d za^(4UCXJJj{X`ET>|xgb2y0vmRD%YEtUOcJn$CEd@pii*L8VNfJ=`hlZi439BZ3bFh9HI3;H z^+fQ`l@Smw3$BxfD0GRV*AN21{V#ZsMlBv4dG9$@!830JWCJ|vs<06lqUp}EEreX_ z)XZb`mQV#0kv6?uR9$_3;nI$xM3WeX2T|O-tyYIwry5&rZh@+OUMf%2uNb(qW)@0u zgl5ksnoQ-_E_`YOku;ME&jb^{;aZ`dEyO1&41k_km%6ZYaEhj=MAia|TcS1O_oQQ5 zYGn{qud~wYTHgerx-R%y@!cwv9d`qd8*r@aWw7%qm6k8&eLuaRU5@DXz&#Z*iv_ws zNBt}v?mDtiB&@|Q`x=dP{ccQKpUy2WOyqY&mD~<`ZF}rxs8lIg zys82Xks%CdSkM(2q9z=L2wm_dvS)C_Ux5s*+^PD0zSu1zcgRcxDK%j8aF}aOG*`

!{8h=xD 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 From 821a8ac06e5ecfd41811218e973cbed472be183d Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Fri, 23 Mar 2012 22:45:46 -0700 Subject: [PATCH 56/75] integrate dmitry kolesnikov's changes to json_encode --- Makefile | 14 ---- priv/b/jsx_b.beam | Bin 1512 -> 0 bytes priv/b/jsx_b.erl | 175 ---------------------------------------------- src/jsx_utils.erl | 167 ++++++++++++++++++------------------------- 4 files changed, 67 insertions(+), 289 deletions(-) delete mode 100644 Makefile delete mode 100644 priv/b/jsx_b.beam delete mode 100644 priv/b/jsx_b.erl diff --git a/Makefile b/Makefile deleted file mode 100644 index e719fcc..0000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ - -all: - test -d ebin || mkdir ebin - erlc -b beam -o ebin src/*.erl - cp src/jsx.app.src ebin/jsx.app - -clean: - rm -Rf ebin - -test: - erlc -b beam -o ebin priv/b/*.erl - -run: - erl -pa ./ebin -pa ./*/ebin diff --git a/priv/b/jsx_b.beam b/priv/b/jsx_b.beam deleted file mode 100644 index cf970ddf11069b74cff639046039032606694ce5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmY*ZO>7%Q6rS;}cXtvy8SmOj4jeWOhqj4hCrzpXmkCvxgoZYP_Rvz~pLp%8v)*;Q z>yQHysZdc`2`OBV7IEmcTnHDCIJM;x3X~q`2@VnBfH+0r|A54M&L)_ZKD}?g`QDrN zc1L^R@+%XB#P?pDoq2gC@EU}WF9AdGy1!8<({kLg{lJ%eYfWmDnp(|r3w7Uf6+dX% zZcVOxwyW4)!LiFq)oV3Mfm(G+L14LR>(HXKN^Zq#sBPCqgW5in<5kb8IK2(8(srzZ z?N&X-YB?ph)?c%NLYJJ%Mj5%uY_;uX^I65Q+*+_AxxtE_J!&X75$*P7%f}oBIqy|0 zLg?Q>IRKv!FaThbhdidA;M7cTE+n}|gft>#L!=}zB8R4Gf?x<)3Qf)AoaV!POfPdP zB0J=C*~PJai<59kHMy80Mp#r$lbd{2iWy>#OJP$r^Wmn(1))pfc1(Y0(qe*}!jQwe zDqn)*y(@GnB$s%T z?i@;--p5fos<}y@gnM92=jmP_bhH0vJik4HGYcaf?jLrs#d)Z4)x6tjUY2xj(EU(L zLEa#z8t>yo3`?P=gFTxxq!*;5F(`(UsZ>&@T1w2BhF~Tb4B51Bg1NjQ)}l2R_(G&v z8G4!%Af`1l4^{R5(lSSBQ0Ei@3kv_sTLN+5FwcNKUXrMm4o&E92r6X+OG^rz0w4O- zpmhZTS!gsdZUK4$xFrK(5syb)g{VcGLF|b*`Z13Jl7PO5qmFkAqYf|t=sDt8A9*Iy z(TDdhHoMX3=Ol2{QUJ_LL>$8L*{H`Zv|a3f5rF<70P0w`^T^uAKLZ&AppHFZB$1v5 zih0;K_C!%%v;(ZU3E1BFaJg74A{vjY#qa3kxy{7;zkL4Vb@O|+`|0S| zXFF5mgNMCSw~X;vdQa)u8GV`=z4kQo#ZM1za7voP&pM0V^3iMx^wK?B_dMM*^n(xQ zz;A``3_0c^aBg`f2wD(NzU`p05~~nt-FG_+my>5c*?sG;`FEdt@6J05NNw~bNzZ}v2L||SgjIiYWy53a$q;C6{Spw`a?-74U~AvD5Y>m@x69y z*;*xMa#wvw%dL8j<-7K3u41iQPR`m0td?7Ha`2nYHCy(2u8g!$9*4hR9h#Ua`vC;9 zXuI(LA_95zm=N*)5O#u%uv6@LmStmXl$~Z**j09omDzRn8hf4HVB;*u&a!DX$tL~< DIn^iu diff --git a/priv/b/jsx_b.erl b/priv/b/jsx_b.erl deleted file mode 100644 index 15865ab..0000000 --- a/priv/b/jsx_b.erl +++ /dev/null @@ -1,175 +0,0 @@ -%% The MIT License - -%% Copyright (c) 2012 Dmitry Kolesnikov - -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: - -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. - -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -%% THE SOFTWARE. --module(jsx_b). - --export([run/2, hot/0]). - --define(LEN_KEY, 32). %% upper bound of object attribute --define(LEN_STR, 256). %% upper bound of string value --define(LEN_INT, 7). %% upper bound of digits --define(JSON, 20). %% number of attributes --define(ALPHA,"qwertyuiopasdfghjklzxcvbnm"). --define(TEXT,"qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\"\\\b\f\n\r\t"). --define(DIGIT,"123456789"). - - -run(Set, Loop) -> - Json = lists:map(fun(_) -> gen_json(?JSON) end, lists:seq(1, Set)), - Term = lists:map(fun(_) -> gen_term(?JSON) end, lists:seq(1, Set)), - [ - { - b_jsx_json(Json, Loop)%, - %b_mochi_json(Json, Loop) - }, - { - b_jsx_term(Term, Loop)%, - %b_mochi_term(Term, Loop) - } - ]. - -hot() -> - b_jsx_term([gen_term(?JSON)], 100). - - -b_jsx_json(Set, Loop) -> - {T, _} = timer:tc( - fun() -> - lists:foreach( - fun(_) -> - lists:map(fun(X) -> jsx:to_term(X) end, Set) - end, - lists:seq(1, Loop) - ) - end, - [] - ), - {jsx, to_term, T / 1000, T / (Loop * length(Set) * 1000)}. - -b_jsx_term(Set, Loop) -> - erlang:garbage_collect(), - {T, _} = timer:tc( - fun() -> - lists:foreach( - fun(_) -> - %error_logger:info_report([{mem_jsx, erlang:memory(processes)}]), - lists:map(fun(X) -> jsx:to_json(X) end, Set) - end, - lists:seq(1, Loop) - ) - end, - [] - ), - {jsx, to_json, T / 1000, T / (Loop * length(Set) * 1000)}. - - -b_mochi_json(Set, Loop) -> - {T, _} = timer:tc( - fun() -> - lists:foreach( - fun(_) -> - lists:map(fun(X) -> mochijson2:decode(X) end, Set) - end, - lists:seq(1, Loop) - ) - end, - [] - ), - {mochi, to_term, T / 1000, T / (Loop * length(Set) * 1000)}. - -b_mochi_term(Set, Loop) -> - erlang:garbage_collect(), - {T, _} = timer:tc( - fun() -> - lists:foreach( - fun(_) -> - %error_logger:info_report([{mem_mochi, erlang:memory(processes)}]), - lists:map(fun(X) -> mochijson2:encode({struct, X})end, Set) - end, - lists:seq(1, Loop) - ) - end, - [] - ), - {mochi, to_json, T / 1000, T / (Loop * length(Set) * 1000)}. - - -%% -%% generates a json object -gen_json(Len) -> - list_to_binary( - io_lib:format("{~s}", [ - string:join( - lists:map( - fun(_) -> - case random:uniform(2) of - 1 -> - io_lib:format("\"~s\":\"~s\"", - [rstring(?LEN_KEY, ?ALPHA), rstring(?LEN_STR, ?ALPHA)] - ); - 2 -> - io_lib:format("\"~s\":~s", - [rstring(?LEN_KEY, ?ALPHA), rstring(?LEN_INT, ?DIGIT)] - ) - end - end, - lists:seq(1,Len) - ), - "," - ) - ]) - ). - -gen_term(Len) -> - lists:map( - fun(_) -> - case random:uniform(2) of - 1 -> { - list_to_binary(rstring(?LEN_KEY, ?ALPHA)), - list_to_binary(rstring(?LEN_STR, ?ALPHA)) - }; - 2 -> { - list_to_binary(rstring(?LEN_KEY, ?ALPHA)), - list_to_integer(rstring(?LEN_INT, ?DIGIT)) - } - end - end, - lists:seq(1,Len) - ). - -%% -%% -rstring(Length, Alphabet) -> - ustring(random:uniform(Length), Alphabet). - -%% -%% from http://blog.teemu.im/2009/11/07/generating-random-strings-in-erlang/ -ustring(Length, AllowedChars) -> - lists:foldl( - fun(_, Acc) -> - [lists:nth( - random:uniform(length(AllowedChars)), - AllowedChars - )] ++ Acc - end, - [], - lists:seq(1, Length) - ). \ No newline at end of file diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index 570d271..c131614 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -29,15 +29,6 @@ -include("jsx_opts.hrl"). --define(ESC(C), - <> -> - B = unicode:characters_to_binary(json_escape_sequence(C)), - json_escape2( - <>, - Opts, L + size(B), Len + size(B) - 1 - ); -). - %% parsing of jsx opts @@ -94,96 +85,63 @@ extract_parser_opts([K|Rest], Acc) -> %% everything else should be a legal json string component json_escape(String, Opts) when is_binary(String) -> - %<< <<(case X of $.->$,; _->X end)>> || <> <= String >>. - %json_escape(String, Opts, <<>>). - json_escape2(String, Opts, 0, size(String)). + json_escape(String, Opts, 0, size(String)). -json_escape2(Str, Opts, L, Len) when L < Len -> - case Str of - <> -> %" - json_escape2(<>, Opts, L + 2, Len + 1);%" - <> -> - json_escape2(<>, Opts, L + 2, Len + 1); - <> -> - json_escape2(<>, Opts, L + 2, Len + 1); - <> -> - json_escape2(<>, Opts, L + 2, Len + 1); - <> -> - json_escape2(<>, Opts, L + 2, Len + 1); - <> -> - json_escape2(<>, Opts, L + 2, Len + 1); - <> -> - json_escape2(<>, Opts, L + 2, Len + 1); - % jsonp - <> -> - B = unicode:characters_to_binary(json_escape_sequence(16#2028)), - json_escape2( - <>, - Opts, L + size(B), Len + size(B) - 1 - ); - <> -> - B = unicode:characters_to_binary(json_escape_sequence(16#2029)), - json_escape2( - <>, - Opts, L + size(B), Len + size(B) - 1 - ); - % C >= 0 and C < $\s - ?ESC(00) ?ESC(01) ?ESC(02) ?ESC(03) ?ESC(04) - ?ESC(05) ?ESC(06) ?ESC(07) - ?ESC(11) ?ESC(14) - ?ESC(15) ?ESC(16) ?ESC(17) ?ESC(18) ?ESC(19) - ?ESC(20) ?ESC(21) ?ESC(22) ?ESC(23) ?ESC(24) - ?ESC(25) ?ESC(26) ?ESC(27) ?ESC(28) ?ESC(29) - ?ESC(30) ?ESC(31) - _ -> - json_escape2(Str, Opts, L + 1, Len) - end; -json_escape2(Str, _, L, Len) when L =:= Len -> - Str. - -%% double quote -json_escape(<<$\", Rest/binary>>, Opts, Acc) -> %" - json_escape(Rest, Opts, <>); %" -%% backslash \ reverse solidus -json_escape(<<$\\, Rest/binary>>, Opts, Acc) -> - json_escape(Rest, Opts, <>); -%% backspace -json_escape(<<$\b, Rest/binary>>, Opts, Acc) -> - json_escape(Rest, Opts, <>); -%% form feed -json_escape(<<$\f, Rest/binary>>, Opts, Acc) -> - json_escape(Rest, Opts, <>); -%% newline -json_escape(<<$\n, Rest/binary>>, Opts, Acc) -> - json_escape(Rest, Opts, <>); -%% cr -json_escape(<<$\r, Rest/binary>>, Opts, Acc) -> - json_escape(Rest, Opts, <>); -%% tab -json_escape(<<$\t, Rest/binary>>, Opts, Acc) -> - json_escape(Rest, Opts, <>); -%% other control characters -json_escape(<>, Opts, Acc) when C >= 0, C < $\s -> - json_escape(Rest, Opts, <>); -%% escape forward slashes -- optionally -- to faciliate microsoft's retarded -%% date format -json_escape(<<$/, Rest/binary>>, Opts=#opts{escape_forward_slash=true}, Acc) -> - json_escape(Rest, Opts, <>); -%% skip escaping u+2028 and u+2029 -json_escape(<>, Opts=#opts{no_jsonp_escapes=true}, Acc) - when C == 16#2028; C == 16#2029 -> - json_escape(Rest, Opts, <>); -%% escape u+2028 and u+2029 to avoid problems with jsonp -json_escape(<>, Opts, Acc) - when C == 16#2028; C == 16#2029 -> - json_escape(Rest, Opts, <>); -%% any other legal codepoint -json_escape(<>, Opts, Acc) -> - json_escape(Rest, Opts, <>); -json_escape(<<>>, _Opts, Acc) -> - Acc; -json_escape(Bin, Opts, Acc) -> - erlang:error(badarg, [Bin, Opts, Acc]). +json_escape(Str, Opts, L, Len) when L < Len -> + case Str of + <> -> %" + json_escape(<>, Opts, L + 2, Len + 1); + <> -> + json_escape(<>, Opts, L + 2, Len + 1); + <> -> + json_escape(<>, Opts, L + 2, Len + 1); + <> -> + json_escape(<>, Opts, L + 2, Len + 1); + <> -> + json_escape(<>, Opts, L + 2, Len + 1); + <> -> + json_escape(<>, Opts, L + 2, Len + 1); + <> -> + json_escape(<>, Opts, L + 2, Len + 1); + <> -> + case Opts#opts.escape_forward_slash of + true -> + json_escape(<>, Opts, L + 2, Len + 1); + false -> + json_escape(<>, Opts, L + 1, Len) + end; + <> -> + case Opts#opts.no_jsonp_escapes of + true -> + json_escape(<>, Opts, L + 3, Len); + false -> + B = unicode:characters_to_binary(json_escape_sequence(16#2028)), + json_escape(<>, Opts, L + size(B), Len + size(B) - size(<<16#2028/utf8>>)) + end; + <> -> + case Opts#opts.no_jsonp_escapes of + true -> + json_escape(<>, Opts, L + 3, Len); + false -> + B = unicode:characters_to_binary(json_escape_sequence(16#2029)), + json_escape(<>, Opts, L + size(B), Len + size(B) - size(<<16#2029/utf8>>)) + end; + <> when X < 32 -> + B = unicode:characters_to_binary(json_escape_sequence(X)), + json_escape(<>, Opts, L + size(B), Len + size(B) - size(<>)); + <<_:L/binary, X/utf8, _/binary>> when X < 16#0080 -> + json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, X/utf8, _/binary>> when X < 16#0800 -> + json_escape(Str, Opts, L + 2, Len); + <<_:L/binary, X/utf8, _/binary>> when X < 16#10000 -> + json_escape(Str, Opts, L + 3, Len); + <<_:L/binary, _/utf8, _/binary>> -> + json_escape(Str, Opts, L + 4, Len); + <> -> + erlang:error(badarg, [[<>, Opts]]) + end; +json_escape(Str, _, L, Len) when L =:= Len -> + Str. %% convert a codepoint to it's \uXXXX equiv. @@ -217,8 +175,8 @@ binary_escape_test_() -> }, {"json string hex escape", ?_assertEqual( - json_escape(<<1, 2, 3, 11, 26, 30, 31>>, #opts{}), - <<"\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> + json_escape(<<0, 1, 2, 3, 11, 26, 30, 31>>, #opts{}), + <<"\\u0000\\u0001\\u0002\\u0003\\u000b\\u001a\\u001e\\u001f">> ) }, {"jsonp protection", @@ -238,6 +196,15 @@ binary_escape_test_() -> json_escape(<<"/Date(1303502009425)/">>, #opts{escape_forward_slash=true}), <<"\\/Date(1303502009425)\\/">> ) + }, + {"bad utf8", + ?_assertError(badarg, json_escape(<<32, 64, 128, 256>>, #opts{})) + }, + {"all sizes of codepoints", + ?_assertEqual( + json_escape(unicode:characters_to_binary([0, 32, 16#80, 16#800, 16#10000]), #opts{}), + <<"\\u0000", 32/utf8, 16#80/utf8, 16#800/utf8, 16#10000/utf8>> + ) } ]. From 5bc8bfdf45ac8e15c77febab04086908fdba5bfa Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Sat, 24 Mar 2012 19:42:00 -0700 Subject: [PATCH 57/75] encoder now only performs a single pass on strings and can optionally json encode them --- src/jsx_encoder.erl | 118 +++++++++++++++++++++++--------------------- src/jsx_opts.hrl | 3 +- src/jsx_to_json.erl | 6 +-- src/jsx_utils.erl | 5 +- 4 files changed, 70 insertions(+), 62 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 5cd9934..18201bf 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -23,7 +23,7 @@ -module(jsx_encoder). --export([encoder/3]). +-export([encoder/3, clean_string/1]). -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). @@ -53,7 +53,7 @@ start(Term, {Handler, State}, Opts) -> value(String, {Handler, State}, Opts) when is_binary(String) -> - Handler:handle_event({string, check_string(String, {Handler, State}, Opts)}, State); + Handler:handle_event({string, clean_string(String, <<>>, Opts)}, State); value(Float, {Handler, State}, _Opts) when is_float(Float) -> Handler:handle_event({float, Float}, State); value(Int, {Handler, State}, _Opts) when is_integer(Int) -> @@ -83,7 +83,7 @@ object([{Key, Value}|Rest], {Handler, State}, Opts) -> Handler, value( Value, - {Handler, Handler:handle_event({key, check_string(fix_key(Key), {Handler, State}, Opts)}, State)}, + {Handler, Handler:handle_event({key, clean_string(fix_key(Key), <<>>, Opts)}, State)}, Opts ) }, @@ -103,21 +103,39 @@ fix_key(Key) when is_atom(Key) -> fix_key(atom_to_binary(Key, utf8)); fix_key(Key) when is_binary(Key) -> Key. -check_string(String, Handler, Opts) -> - case check_string(String) of - true -> String; - false -> - case Opts#opts.loose_unicode of - true -> clean_string(String, <<>>); - false -> erlang:error(badarg, [String, Handler, Opts]) - end - end. +clean_string(Bin) -> clean_string(Bin, <<>>, #opts{json_escape=true}). -check_string(<>) when C < 16#fdd0 -> - check_string(Rest); -check_string(<>) when C > 16#fdef, C < 16#fffe -> - check_string(Rest); -check_string(<>) +clean_string(<<$\", Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<$\\, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<$\b, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<$\f, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<$\n, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<$\r, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<$\t, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<$/, Rest/binary>>, Acc, Opts=#opts{json_escape=true, escape_forward_slash=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<16#2028/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true, no_jsonp_escapes=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<16#2029/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true, no_jsonp_escapes=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<16#2028/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<16#2029/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<>, Acc, Opts=#opts{json_escape=true}) when C < 32 -> + clean_string(Rest, <>, Opts); +clean_string(<>, Acc, Opts) when C < 16#fdd0 -> + clean_string(Rest, <>, Opts); +clean_string(<>, Acc, Opts) when C > 16#fdef, C < 16#fffe -> + clean_string(Rest, <>, Opts); +clean_string(<>, Acc, Opts) when C > 16#ffff andalso C =/= 16#1fffe andalso C =/= 16#1ffff andalso C =/= 16#2fffe andalso C =/= 16#2ffff andalso @@ -135,46 +153,18 @@ check_string(<>) C =/= 16#efffe andalso C =/= 16#effff andalso C =/= 16#ffffe andalso C =/= 16#fffff andalso C =/= 16#10fffe andalso C =/= 16#10ffff -> - check_string(Rest); -check_string(<<>>) -> true; -check_string(<<_, _/binary>>) -> false. - - -clean_string(<>, Acc) when C < 16#fdd0 -> - clean_string(Rest, <>); -clean_string(<>, Acc) when C > 16#fdef, C < 16#fffe -> - clean_string(Rest, <>); -clean_string(<>, Acc) - when C > 16#ffff andalso - C =/= 16#1fffe andalso C =/= 16#1ffff andalso - C =/= 16#2fffe andalso C =/= 16#2ffff andalso - C =/= 16#3fffe andalso C =/= 16#3ffff andalso - C =/= 16#4fffe andalso C =/= 16#4ffff andalso - C =/= 16#5fffe andalso C =/= 16#5ffff andalso - C =/= 16#6fffe andalso C =/= 16#6ffff andalso - C =/= 16#7fffe andalso C =/= 16#7ffff andalso - C =/= 16#8fffe andalso C =/= 16#8ffff andalso - C =/= 16#9fffe andalso C =/= 16#9ffff andalso - C =/= 16#afffe andalso C =/= 16#affff andalso - C =/= 16#bfffe andalso C =/= 16#bffff andalso - C =/= 16#cfffe andalso C =/= 16#cffff andalso - C =/= 16#dfffe andalso C =/= 16#dffff andalso - C =/= 16#efffe andalso C =/= 16#effff andalso - C =/= 16#ffffe andalso C =/= 16#fffff andalso - C =/= 16#10fffe andalso C =/= 16#10ffff -> - clean_string(Rest, <>); + clean_string(Rest, <>, Opts); %% surrogates -clean_string(<<237, X, _, Rest/binary>>, Acc) when X >= 160 -> - clean_string(Rest, <>); +clean_string(<<237, X, _, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) when X >= 160 -> + clean_string(Rest, <>, Opts); %% private use noncharacters -clean_string(<<239, 183, X, Rest/binary>>, Acc) when X >= 143, X =< 175 -> - clean_string(Rest, <>); +clean_string(<<239, 183, X, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) when X >= 143, X =< 175 -> + clean_string(Rest, <>, Opts); %% u+fffe and u+ffff -clean_string(<<239, 191, X, Rest/binary>>, Acc) when X == 190; X == 191 -> - clean_string(Rest, <>); +clean_string(<<239, 191, X, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) when X == 190; X == 191 -> + clean_string(Rest, <>, Opts); %% the u+Xfffe and u+Xffff noncharacters -clean_string(<>, Acc) - when ( +clean_string(<>, Acc, Opts=#opts{loose_unicode=true}) when ( (X == 240 andalso Y == 159) orelse (X == 240 andalso Y == 175) orelse (X == 240 andalso Y == 191) orelse @@ -184,13 +174,27 @@ clean_string(<>, Acc) ) orelse (X == 244 andalso Y == 143) ) andalso (Z == 190 orelse Z == 191) -> - clean_string(Rest, <>); -clean_string(<<_, Rest/binary>>, Acc) -> - clean_string(Rest, <>); -clean_string(<<>>, Acc) -> Acc. + clean_string(Rest, <>, Opts); +clean_string(<<_, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) -> + clean_string(Rest, <>, Opts); +clean_string(<<>>, Acc, _) -> Acc; +clean_string(Bin, _Acc, Opts) -> erlang:error(badarg, [Bin, Opts]). + +%% convert a codepoint to it's \uXXXX equiv. +json_escape_sequence(X) -> + <> = <>, + unicode:characters_to_binary([$\\, $u, (to_hex(A)), (to_hex(B)), (to_hex(C)), (to_hex(D))]). +to_hex(10) -> $a; +to_hex(11) -> $b; +to_hex(12) -> $c; +to_hex(13) -> $d; +to_hex(14) -> $e; +to_hex(15) -> $f; +to_hex(X) -> X + 48. %% ascii "1" is [49], "2" is [50], etc... + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/jsx_opts.hrl b/src/jsx_opts.hrl index 7741585..a184dbe 100644 --- a/src/jsx_opts.hrl +++ b/src/jsx_opts.hrl @@ -4,5 +4,6 @@ explicit_end = false, single_quotes = false, no_jsonp_escapes = false, - comments = false + comments = false, + json_escape = false }). \ No newline at end of file diff --git a/src/jsx_to_json.erl b/src/jsx_to_json.erl index 0e4edf4..4d2ea14 100644 --- a/src/jsx_to_json.erl +++ b/src/jsx_to_json.erl @@ -39,7 +39,7 @@ -spec to_json(Source::any(), Opts::opts()) -> binary(). to_json(Source, Opts) when is_list(Opts) -> - (jsx:encoder(?MODULE, Opts, jsx_utils:extract_opts(Opts)))(Source). + (jsx:encoder(?MODULE, Opts, jsx_utils:extract_opts([json_escape] ++ Opts)))(Source). -spec format(Source::binary(), Opts::opts()) -> binary(). @@ -135,8 +135,8 @@ handle_event(Event, {[array|Stack], Acc, Opts = #opts{depth = Depth}}) -> handle_event(end_json, {[], Acc, _Opts}) -> unicode:characters_to_binary(Acc, utf8). -encode(string, String, Opts) -> - [?quote, jsx_utils:json_escape(String, Opts), ?quote]; +encode(string, String, _Opts) -> + [?quote, String, ?quote]; encode(literal, Literal, _Opts) -> erlang:atom_to_list(Literal); encode(integer, Integer, _Opts) -> diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index c131614..f9970ee 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -49,6 +49,8 @@ parse_opts([no_jsonp_escapes|Rest], Opts) -> parse_opts(Rest, Opts#opts{no_jsonp_escapes=true}); parse_opts([comments|Rest], Opts) -> parse_opts(Rest, Opts#opts{comments=true}); +parse_opts([json_escape|Rest], Opts) -> + parse_opts(Rest, Opts#opts{json_escape=true}); parse_opts(_, _) -> {error, badarg}. @@ -60,7 +62,8 @@ valid_flags() -> explicit_end, single_quotes, no_jsonp_escapes, - comments + comments, + json_escape ]. From b57750fcfc9f2cf94a2228c941caea0e74ac85f4 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Sun, 25 Mar 2012 13:18:26 -0700 Subject: [PATCH 58/75] fix specs for to_term/x, thanks to michael truog --- src/jsx.erl | 7 ++----- src/jsx_to_term.erl | 11 +++++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/jsx.erl b/src/jsx.erl index 79682b9..d700bd5 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -60,11 +60,8 @@ format(Source) -> format(Source, []). format(Source, Opts) -> jsx_to_json:format(Source, Opts). --spec to_term(Source::binary()) -> - list({binary(), any()}). --spec to_term(Source::binary(), Opts::jsx_to_term:opts()) -> - list({binary(), any()}). - +-spec to_term(Source::binary()) -> jsx_to_term:json_value(). +-spec to_term(Source::binary(), Opts::jsx_to_term:opts()) -> jsx_to_term:json_value(). to_term(Source) -> to_term(Source, []). diff --git a/src/jsx_to_term.erl b/src/jsx_to_term.erl index 86e9882..4875fee 100644 --- a/src/jsx_to_term.erl +++ b/src/jsx_to_term.erl @@ -33,9 +33,16 @@ -type opts() :: list(). +-type json_value() :: list({binary(), json_value()}) + | list(json_value()) + | true + | false + | null + | integer() + | float() + | binary(). --spec to_term(Source::(binary() | list()), Opts::opts()) -> - list({binary(), any()}). +-spec to_term(Source::binary(), Opts::opts()) -> json_value(). to_term(Source, Opts) when is_list(Opts) -> (jsx:decoder(?MODULE, Opts, jsx_utils:extract_opts(Opts)))(Source). From 02489dc75276840c2128a1978e5f69bbb8996c52 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Sun, 25 Mar 2012 13:19:22 -0700 Subject: [PATCH 59/75] updates contribs --- README.markdown | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index 1fffe9b..707df14 100644 --- a/README.markdown +++ b/README.markdown @@ -301,8 +301,7 @@ types: ## acknowledgements ## -paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides, alex kropivny and steve strong have all contributed to the development of jsx, whether they know it or not - +jsx wouldn't be what it is without the contributions of paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides, alex kropivny, steve strong and michael truog [json]: http://json.org [yajl]: http://lloyd.github.com/yajl From bc588ceb7b0b12504e5ea0242bee07d6425ae84a Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Sun, 25 Mar 2012 18:48:24 -0700 Subject: [PATCH 60/75] first step to better perf --- src/jsx_decoder.erl | 199 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 7 deletions(-) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index f70e0f8..bf20af6 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -145,11 +145,11 @@ value(<<$f, Rest/binary>>, Handler, Stack, Opts) -> value(<<$n, Rest/binary>>, Handler, Stack, Opts) -> nu(Rest, Handler, Stack, Opts); value(<>, Handler, Stack, Opts) -> - negative(Rest, Handler, [?new_seq($-)|Stack], Opts); + negative(Rest, Handler, [[$-]|Stack], Opts); value(<>, Handler, Stack, Opts) -> - zero(Rest, Handler, [?new_seq($0)|Stack], Opts); + zero(Rest, Handler, [[$0]|Stack], Opts); value(<>, Handler, Stack, Opts) when ?is_nonzero(S) -> - integer(Rest, Handler, [?new_seq(S)|Stack], Opts); + integer(Rest, Handler, [[S]|Stack], Opts); value(<>, {Handler, State}, Stack, Opts) -> object(Rest, {Handler, Handler:handle_event(start_object, State)}, [key|Stack], Opts); value(<>, {Handler, State}, Stack, Opts) -> @@ -193,11 +193,11 @@ array(<<$f, Rest/binary>>, Handler, Stack, Opts) -> array(<<$n, Rest/binary>>, Handler, Stack, Opts) -> nu(Rest, Handler, Stack, Opts); array(<>, Handler, Stack, Opts) -> - negative(Rest, Handler, [?new_seq($-)|Stack], Opts); + negative(Rest, Handler, [[$-]|Stack], Opts); array(<>, Handler, Stack, Opts) -> - zero(Rest, Handler, [?new_seq($0)|Stack], Opts); + zero(Rest, Handler, [[$0]|Stack], Opts); array(<>, Handler, Stack, Opts) when ?is_nonzero(S) -> - integer(Rest, Handler, [?new_seq(S)|Stack], Opts); + integer(Rest, Handler, [[S]|Stack], Opts); array(<>, {Handler, State}, Stack, Opts) -> object(Rest, {Handler, Handler:handle_event(start_object, State)}, [key|Stack], Opts); array(<>, {Handler, State}, Stack, Opts) -> @@ -259,7 +259,10 @@ partial_utf(<>) true; partial_utf(_) -> false. - +string(<<32, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 32)|Stack], Opts); +string(<<33, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 33)|Stack], Opts); string(<>, {Handler, State}, S, Opts) -> case S of [Acc, key|Stack] -> @@ -269,6 +272,14 @@ string(<>, {Handler, State}, S, Opts) -> [Acc|Stack] -> maybe_done(Rest, {Handler, Handler:handle_event({string, ?end_seq(Acc)}, State)}, Stack, Opts) end; +string(<<35, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 35)|Stack], Opts); +string(<<36, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 36)|Stack], Opts); +string(<<37, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 37)|Stack], Opts); +string(<<38, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 38)|Stack], Opts); string(<>, {Handler, State}, S, Opts = #opts{single_quotes=true}) -> case S of [Acc, single_quote, key|Stack] -> @@ -278,8 +289,182 @@ string(<>, {Handler, State}, S, Opts = #opts{single_q [Acc|Stack] -> string(Rest, {Handler, State}, [?acc_seq(Acc, ?singlequote)|Stack], Opts) end; +string(<<40, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 40)|Stack], Opts); +string(<<41, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 41)|Stack], Opts); +string(<<42, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 42)|Stack], Opts); +string(<<43, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 43)|Stack], Opts); +string(<<44, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 44)|Stack], Opts); +string(<<45, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 45)|Stack], Opts); +string(<<46, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 46)|Stack], Opts); +string(<<47, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 47)|Stack], Opts); +string(<<48, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 48)|Stack], Opts); +string(<<49, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 49)|Stack], Opts); +string(<<50, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 50)|Stack], Opts); +string(<<51, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 51)|Stack], Opts); +string(<<52, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 52)|Stack], Opts); +string(<<53, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 53)|Stack], Opts); +string(<<54, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 54)|Stack], Opts); +string(<<55, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 55)|Stack], Opts); +string(<<56, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 56)|Stack], Opts); +string(<<57, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 57)|Stack], Opts); +string(<<58, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 58)|Stack], Opts); +string(<<59, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 59)|Stack], Opts); +string(<<60, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 60)|Stack], Opts); +string(<<61, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 61)|Stack], Opts); +string(<<62, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 62)|Stack], Opts); +string(<<63, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 63)|Stack], Opts); +string(<<64, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 64)|Stack], Opts); +string(<<65, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 65)|Stack], Opts); +string(<<66, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 66)|Stack], Opts); +string(<<67, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 67)|Stack], Opts); +string(<<68, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 68)|Stack], Opts); +string(<<69, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 69)|Stack], Opts); +string(<<70, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 70)|Stack], Opts); +string(<<71, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 71)|Stack], Opts); +string(<<72, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 72)|Stack], Opts); +string(<<73, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 73)|Stack], Opts); +string(<<74, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 74)|Stack], Opts); +string(<<75, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 75)|Stack], Opts); +string(<<76, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 76)|Stack], Opts); +string(<<77, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 77)|Stack], Opts); +string(<<78, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 78)|Stack], Opts); +string(<<79, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 79)|Stack], Opts); +string(<<80, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 80)|Stack], Opts); +string(<<81, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 81)|Stack], Opts); +string(<<82, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 82)|Stack], Opts); +string(<<83, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 83)|Stack], Opts); +string(<<84, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 84)|Stack], Opts); +string(<<85, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 85)|Stack], Opts); +string(<<86, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 86)|Stack], Opts); +string(<<87, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 87)|Stack], Opts); +string(<<88, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 88)|Stack], Opts); +string(<<89, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 89)|Stack], Opts); +string(<<90, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 90)|Stack], Opts); +string(<<91, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 91)|Stack], Opts); string(<>, Handler, Stack, Opts) -> escape(Rest, Handler, Stack, Opts); +string(<<93, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 93)|Stack], Opts); +string(<<94, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 94)|Stack], Opts); +string(<<95, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 95)|Stack], Opts); +string(<<96, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 96)|Stack], Opts); +string(<<97, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 97)|Stack], Opts); +string(<<98, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 98)|Stack], Opts); +string(<<99, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 99)|Stack], Opts); +string(<<100, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 100)|Stack], Opts); +string(<<101, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 101)|Stack], Opts); +string(<<102, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 102)|Stack], Opts); +string(<<103, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 103)|Stack], Opts); +string(<<104, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 104)|Stack], Opts); +string(<<105, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 105)|Stack], Opts); +string(<<106, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 106)|Stack], Opts); +string(<<107, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 107)|Stack], Opts); +string(<<108, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 108)|Stack], Opts); +string(<<109, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 109)|Stack], Opts); +string(<<110, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 110)|Stack], Opts); +string(<<111, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 111)|Stack], Opts); +string(<<112, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 112)|Stack], Opts); +string(<<113, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 113)|Stack], Opts); +string(<<114, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 114)|Stack], Opts); +string(<<115, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 115)|Stack], Opts); +string(<<116, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 116)|Stack], Opts); +string(<<117, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 117)|Stack], Opts); +string(<<118, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 118)|Stack], Opts); +string(<<119, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 119)|Stack], Opts); +string(<<120, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 120)|Stack], Opts); +string(<<121, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 121)|Stack], Opts); +string(<<122, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 122)|Stack], Opts); +string(<<123, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 123)|Stack], Opts); +string(<<124, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 124)|Stack], Opts); +string(<<125, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 125)|Stack], Opts); +string(<<126, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 126)|Stack], Opts); +string(<<127, Rest/binary>>, Handler, [Acc|Stack], Opts) -> + string(Rest, Handler, [?acc_seq(Acc, 127)|Stack], Opts); %% things get dumb here. erlang doesn't properly restrict unicode non-characters %% so you can't trust the codepoints it returns always %% the range 32..16#fdcf is safe, so allow that From f6e1e7516354050fdcb5cd493a5d0b19811ad291 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Sun, 25 Mar 2012 18:49:24 -0700 Subject: [PATCH 61/75] remove clean_string/1 --- src/jsx_encoder.erl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 18201bf..76e9111 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -23,7 +23,7 @@ -module(jsx_encoder). --export([encoder/3, clean_string/1]). +-export([encoder/3]). -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). @@ -103,8 +103,6 @@ fix_key(Key) when is_atom(Key) -> fix_key(atom_to_binary(Key, utf8)); fix_key(Key) when is_binary(Key) -> Key. -clean_string(Bin) -> clean_string(Bin, <<>>, #opts{json_escape=true}). - clean_string(<<$\", Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> clean_string(Rest, <>, Opts); clean_string(<<$\\, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> From 41002bd10f559763d88636ece0f983257d54427d Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 26 Mar 2012 18:52:06 -0700 Subject: [PATCH 62/75] comment explaining weird string functions --- src/jsx_decoder.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index bf20af6..b8a58b6 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -259,6 +259,9 @@ partial_utf(<>) true; partial_utf(_) -> false. + +%% explicitly whitelist ascii set for better efficiency (seriously, it's worth +%% almost a 20% increase) string(<<32, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, 32)|Stack], Opts); string(<<33, Rest/binary>>, Handler, [Acc|Stack], Opts) -> From 04ea83266ed77b3592e805d0fbe871d4fc9f3e8a Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 26 Mar 2012 18:52:32 -0700 Subject: [PATCH 63/75] slightly more efficient encoding of strings --- src/jsx_encoder.erl | 164 +++++++------------------------------------- 1 file changed, 26 insertions(+), 138 deletions(-) diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 18201bf..1dbd13f 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -23,7 +23,7 @@ -module(jsx_encoder). --export([encoder/3, clean_string/1]). +-export([encoder/3]). -spec encoder(Handler::module(), State::any(), Opts::jsx:opts()) -> jsx:encoder(). @@ -53,7 +53,7 @@ start(Term, {Handler, State}, Opts) -> value(String, {Handler, State}, Opts) when is_binary(String) -> - Handler:handle_event({string, clean_string(String, <<>>, Opts)}, State); + Handler:handle_event({string, clean_string(String, Opts)}, State); value(Float, {Handler, State}, _Opts) when is_float(Float) -> Handler:handle_event({float, Float}, State); value(Int, {Handler, State}, _Opts) when is_integer(Int) -> @@ -83,7 +83,7 @@ object([{Key, Value}|Rest], {Handler, State}, Opts) -> Handler, value( Value, - {Handler, Handler:handle_event({key, clean_string(fix_key(Key), <<>>, Opts)}, State)}, + {Handler, Handler:handle_event({key, clean_string(fix_key(Key), Opts)}, State)}, Opts ) }, @@ -103,98 +103,31 @@ fix_key(Key) when is_atom(Key) -> fix_key(atom_to_binary(Key, utf8)); fix_key(Key) when is_binary(Key) -> Key. -clean_string(Bin) -> clean_string(Bin, <<>>, #opts{json_escape=true}). +clean_string(Bin, Opts) -> + case Opts#opts.json_escape of + true -> jsx_utils:json_escape(Bin, Opts); + false -> + case is_clean(Bin) of + true -> Bin; + false -> clean_string(Bin, [], Opts) + end + end. -clean_string(<<$\", Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<$\\, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<$\b, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<$\f, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<$\n, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<$\r, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<$\t, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<$/, Rest/binary>>, Acc, Opts=#opts{json_escape=true, escape_forward_slash=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<16#2028/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true, no_jsonp_escapes=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<16#2029/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true, no_jsonp_escapes=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<16#2028/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<16#2029/utf8, Rest/binary>>, Acc, Opts=#opts{json_escape=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<>, Acc, Opts=#opts{json_escape=true}) when C < 32 -> - clean_string(Rest, <>, Opts); -clean_string(<>, Acc, Opts) when C < 16#fdd0 -> - clean_string(Rest, <>, Opts); -clean_string(<>, Acc, Opts) when C > 16#fdef, C < 16#fffe -> - clean_string(Rest, <>, Opts); -clean_string(<>, Acc, Opts) - when C > 16#ffff andalso - C =/= 16#1fffe andalso C =/= 16#1ffff andalso - C =/= 16#2fffe andalso C =/= 16#2ffff andalso - C =/= 16#3fffe andalso C =/= 16#3ffff andalso - C =/= 16#4fffe andalso C =/= 16#4ffff andalso - C =/= 16#5fffe andalso C =/= 16#5ffff andalso - C =/= 16#6fffe andalso C =/= 16#6ffff andalso - C =/= 16#7fffe andalso C =/= 16#7ffff andalso - C =/= 16#8fffe andalso C =/= 16#8ffff andalso - C =/= 16#9fffe andalso C =/= 16#9ffff andalso - C =/= 16#afffe andalso C =/= 16#affff andalso - C =/= 16#bfffe andalso C =/= 16#bffff andalso - C =/= 16#cfffe andalso C =/= 16#cffff andalso - C =/= 16#dfffe andalso C =/= 16#dffff andalso - C =/= 16#efffe andalso C =/= 16#effff andalso - C =/= 16#ffffe andalso C =/= 16#fffff andalso - C =/= 16#10fffe andalso C =/= 16#10ffff -> - clean_string(Rest, <>, Opts); + +is_clean(<<>>) -> true; +is_clean(<<_/utf8, Rest/binary>>) -> is_clean(Rest); +is_clean(_) -> false. + + +clean_string(Bin, _Acc, Opts=#opts{loose_unicode=false}) -> ?error([Bin, Opts]); +clean_string(<<>>, Acc, _Opts) -> unicode:characters_to_binary(lists:reverse(Acc)); +clean_string(<>, Acc, Opts) -> clean_string(Rest, [X] ++ Acc, Opts); %% surrogates -clean_string(<<237, X, _, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) when X >= 160 -> - clean_string(Rest, <>, Opts); -%% private use noncharacters -clean_string(<<239, 183, X, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) when X >= 143, X =< 175 -> - clean_string(Rest, <>, Opts); -%% u+fffe and u+ffff -clean_string(<<239, 191, X, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) when X == 190; X == 191 -> - clean_string(Rest, <>, Opts); -%% the u+Xfffe and u+Xffff noncharacters -clean_string(<>, Acc, Opts=#opts{loose_unicode=true}) when ( - (X == 240 andalso Y == 159) orelse - (X == 240 andalso Y == 175) orelse - (X == 240 andalso Y == 191) orelse - ( - (X == 241 orelse X == 242 orelse X == 243) andalso - (Y == 143 orelse Y == 159 orelse Y == 175 orelse Y == 191) - ) orelse - (X == 244 andalso Y == 143) - ) andalso (Z == 190 orelse Z == 191) -> - clean_string(Rest, <>, Opts); -clean_string(<<_, Rest/binary>>, Acc, Opts=#opts{loose_unicode=true}) -> - clean_string(Rest, <>, Opts); -clean_string(<<>>, Acc, _) -> Acc; -clean_string(Bin, _Acc, Opts) -> erlang:error(badarg, [Bin, Opts]). - - -%% convert a codepoint to it's \uXXXX equiv. -json_escape_sequence(X) -> - <> = <>, - unicode:characters_to_binary([$\\, $u, (to_hex(A)), (to_hex(B)), (to_hex(C)), (to_hex(D))]). +clean_string(<<237, X, _, Rest/binary>>, Acc, Opts) when X >= 160 -> clean_string(Rest, [16#fffd] ++ Acc, Opts); +%% bad codepoints +clean_string(<<_, Rest/binary>>, Acc, Opts) -> clean_string(Rest, [16#fffd] ++ Acc, Opts). -to_hex(10) -> $a; -to_hex(11) -> $b; -to_hex(12) -> $c; -to_hex(13) -> $d; -to_hex(14) -> $e; -to_hex(15) -> $f; -to_hex(X) -> X + 48. %% ascii "1" is [49], "2" is [50], etc... - -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -276,26 +209,6 @@ encode_test_() -> ) } ]. - -noncharacters_test_() -> - [ - {"noncharacters - badjson", - ?_assertEqual(check_bad(noncharacters()), []) - }, - {"noncharacters - replaced", - ?_assertEqual(check_replaced(noncharacters()), []) - } - ]. - -extended_noncharacters_test_() -> - [ - {"extended noncharacters - badjson", - ?_assertEqual(check_bad(extended_noncharacters()), []) - }, - {"extended noncharacters - replaced", - ?_assertEqual(check_replaced(extended_noncharacters()), []) - } - ]. surrogates_test_() -> [ @@ -306,16 +219,6 @@ surrogates_test_() -> ?_assertEqual(check_replaced(surrogates()), []) } ]. - -reserved_test_() -> - [ - {"reserved noncharacters - badjson", - ?_assertEqual(check_bad(reserved_space()), []) - }, - {"reserved noncharacters - replaced", - ?_assertEqual(check_replaced(reserved_space()), []) - } - ]. good_characters_test_() -> [ @@ -387,26 +290,11 @@ check([H|T], Opts, Acc) -> check(T, Opts, [{H, R}] ++ Acc). - -noncharacters() -> lists:seq(16#fffe, 16#ffff). - -extended_noncharacters() -> - [16#1fffe, 16#1ffff, 16#2fffe, 16#2ffff] - ++ [16#3fffe, 16#3ffff, 16#4fffe, 16#4ffff] - ++ [16#5fffe, 16#5ffff, 16#6fffe, 16#6ffff] - ++ [16#7fffe, 16#7ffff, 16#8fffe, 16#8ffff] - ++ [16#9fffe, 16#9ffff, 16#afffe, 16#affff] - ++ [16#bfffe, 16#bffff, 16#cfffe, 16#cffff] - ++ [16#dfffe, 16#dffff, 16#efffe, 16#effff] - ++ [16#ffffe, 16#fffff, 16#10fffe, 16#10ffff]. - surrogates() -> lists:seq(16#d800, 16#dfff). -reserved_space() -> lists:seq(16#fdd0, 16#fdef). - -good() -> lists:seq(1, 16#d7ff) ++ lists:seq(16#e000, 16#fdcf) ++ lists:seq(16#fdf0, 16#fffd). +good() -> lists:seq(1, 16#d7ff) ++ lists:seq(16#e000, 16#ffff). -good_extended() -> lists:seq(16#100000, 16#10fffd). +good_extended() -> lists:seq(16#100000, 16#10ffff). %% erlang refuses to encode certain codepoints, so fake them all to_fake_utf(N, utf8) when N < 16#0080 -> <>; From 3421b6546ed539216f5b391cb4e0ff345fc65044 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 26 Mar 2012 19:28:53 -0700 Subject: [PATCH 64/75] rewrite of json_escape for efficiency --- src/jsx_utils.erl | 182 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 162 insertions(+), 20 deletions(-) diff --git a/src/jsx_utils.erl b/src/jsx_utils.erl index f9970ee..462e31d 100644 --- a/src/jsx_utils.erl +++ b/src/jsx_utils.erl @@ -90,22 +90,66 @@ extract_parser_opts([K|Rest], Acc) -> json_escape(String, Opts) when is_binary(String) -> json_escape(String, Opts, 0, size(String)). + +-define(control_character(X), + <> -> + json_escape( + <>, + Opts, + L + 6, + Len + 5 + ) +). + json_escape(Str, Opts, L, Len) when L < Len -> case Str of - <> -> %" - json_escape(<>, Opts, L + 2, Len + 1); - <> -> - json_escape(<>, Opts, L + 2, Len + 1); - <> -> - json_escape(<>, Opts, L + 2, Len + 1); - <> -> - json_escape(<>, Opts, L + 2, Len + 1); - <> -> - json_escape(<>, Opts, L + 2, Len + 1); - <> -> - json_escape(<>, Opts, L + 2, Len + 1); - <> -> - json_escape(<>, Opts, L + 2, Len + 1); + ?control_character(0); + ?control_character(1); + ?control_character(2); + ?control_character(3); + ?control_character(4); + ?control_character(5); + ?control_character(6); + ?control_character(7); + <> -> json_escape(<>, Opts, L + 2, Len + 1); + <> -> json_escape(<>, Opts, L + 2, Len + 1); + <> -> json_escape(<>, Opts, L + 2, Len + 1); + ?control_character(11); + <> -> json_escape(<>, Opts, L + 2, Len + 1); + <> -> json_escape(<>, Opts, L + 2, Len + 1); + ?control_character(14); + ?control_character(15); + ?control_character(16); + ?control_character(17); + ?control_character(18); + ?control_character(19); + ?control_character(20); + ?control_character(21); + ?control_character(22); + ?control_character(23); + ?control_character(24); + ?control_character(25); + ?control_character(26); + ?control_character(27); + ?control_character(28); + ?control_character(29); + ?control_character(30); + ?control_character(31); + <<_:L/binary, 32, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 33, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <> -> json_escape(<>, Opts, L + 2, Len + 1); + <<_:L/binary, 35, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 36, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 37, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 38, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 39, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 40, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 41, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 42, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 43, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 44, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 45, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 46, _/binary>> -> json_escape(Str, Opts, L + 1, Len); <> -> case Opts#opts.escape_forward_slash of true -> @@ -113,6 +157,86 @@ json_escape(Str, Opts, L, Len) when L < Len -> false -> json_escape(<>, Opts, L + 1, Len) end; + <<_:L/binary, 48, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 49, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 50, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 51, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 52, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 53, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 54, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 55, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 56, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 57, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 58, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 59, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 60, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 61, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 62, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 63, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 64, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 65, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 66, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 67, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 68, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 69, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 70, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 71, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 72, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 73, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 74, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 75, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 76, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 77, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 78, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 79, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 80, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 81, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 82, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 83, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 84, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 85, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 86, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 87, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 88, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 89, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 90, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 91, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <> -> json_escape(<>, Opts, L + 2, Len + 1); + <<_:L/binary, 93, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 94, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 95, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 96, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 97, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 98, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 99, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 100, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 101, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 102, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 103, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 104, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 105, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 106, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 107, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 108, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 109, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 110, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 111, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 112, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 113, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 114, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 115, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 116, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 117, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 118, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 119, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 120, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 121, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 122, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 123, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 124, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 125, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 126, _/binary>> -> json_escape(Str, Opts, L + 1, Len); + <<_:L/binary, 127, _/binary>> -> json_escape(Str, Opts, L + 1, Len); <> -> case Opts#opts.no_jsonp_escapes of true -> @@ -129,9 +253,6 @@ json_escape(Str, Opts, L, Len) when L < Len -> B = unicode:characters_to_binary(json_escape_sequence(16#2029)), json_escape(<>, Opts, L + size(B), Len + size(B) - size(<<16#2029/utf8>>)) end; - <> when X < 32 -> - B = unicode:characters_to_binary(json_escape_sequence(X)), - json_escape(<>, Opts, L + size(B), Len + size(B) - size(<>)); <<_:L/binary, X/utf8, _/binary>> when X < 16#0080 -> json_escape(Str, Opts, L + 1, Len); <<_:L/binary, X/utf8, _/binary>> when X < 16#0800 -> @@ -140,8 +261,16 @@ json_escape(Str, Opts, L, Len) when L < Len -> json_escape(Str, Opts, L + 3, Len); <<_:L/binary, _/utf8, _/binary>> -> json_escape(Str, Opts, L + 4, Len); - <> -> - erlang:error(badarg, [[<>, Opts]]) + <> when X >= 160 -> + case Opts#opts.loose_unicode of + true -> json_escape(<>, Opts, L + 3, Len); + false -> erlang:error(badarg, [Str, Opts]) + end; + <> -> + case Opts#opts.loose_unicode of + true -> json_escape(<>, Opts, L + 3, Len + 2); + false -> erlang:error(badarg, [Str, Opts]) + end end; json_escape(Str, _, L, Len) when L =:= Len -> Str. @@ -201,7 +330,20 @@ binary_escape_test_() -> ) }, {"bad utf8", - ?_assertError(badarg, json_escape(<<32, 64, 128, 256>>, #opts{})) + ?_assertError(badarg, json_escape(<<32, 64, 128, 255>>, #opts{})) + }, + {"bad utf8 ok", + ?_assertEqual( + json_escape(<<32, 64, 128, 255>>, #opts{loose_unicode=true}), + <<32, 64, 16#fffd/utf8, 16#fffd/utf8>> + ) + }, + {"bad surrogate", ?_assertError(badarg, json_escape(<<237, 160, 127>>, #opts{}))}, + {"bad surrogate ok", + ?_assertEqual( + json_escape(<<237, 160, 127>>, #opts{loose_unicode=true}), + <<16#fffd/utf8>> + ) }, {"all sizes of codepoints", ?_assertEqual( From f1c4a85df1b01968e6776e48e2513d2b784355bf Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 26 Mar 2012 19:39:28 -0700 Subject: [PATCH 65/75] loosen restrictions on allowed codepoints in strings --- priv/test_cases/noncharacter.json | 1 - priv/test_cases/noncharacter.test | 3 - priv/test_cases/noncharacter_replaced.json | 1 - priv/test_cases/noncharacter_replaced.test | 4 - src/jsx_decoder.erl | 113 ++------------------- 5 files changed, 6 insertions(+), 116 deletions(-) delete mode 100644 priv/test_cases/noncharacter.json delete mode 100644 priv/test_cases/noncharacter.test delete mode 100644 priv/test_cases/noncharacter_replaced.json delete mode 100644 priv/test_cases/noncharacter_replaced.test diff --git a/priv/test_cases/noncharacter.json b/priv/test_cases/noncharacter.json deleted file mode 100644 index 09db417..0000000 --- a/priv/test_cases/noncharacter.json +++ /dev/null @@ -1 +0,0 @@ -"﷐" \ No newline at end of file diff --git a/priv/test_cases/noncharacter.test b/priv/test_cases/noncharacter.test deleted file mode 100644 index 6b3732c..0000000 --- a/priv/test_cases/noncharacter.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "noncharacter"}. -{jsx, {error, badjson}}. -{json, "noncharacter.json"}. \ No newline at end of file diff --git a/priv/test_cases/noncharacter_replaced.json b/priv/test_cases/noncharacter_replaced.json deleted file mode 100644 index 09db417..0000000 --- a/priv/test_cases/noncharacter_replaced.json +++ /dev/null @@ -1 +0,0 @@ -"﷐" \ No newline at end of file diff --git a/priv/test_cases/noncharacter_replaced.test b/priv/test_cases/noncharacter_replaced.test deleted file mode 100644 index 0944886..0000000 --- a/priv/test_cases/noncharacter_replaced.test +++ /dev/null @@ -1,4 +0,0 @@ -{name, "noncharacter replaced"}. -{jsx, [{string,<<16#fffd/utf8>>},end_json]}. -{json, "noncharacter_replaced.json"}. -{jsx_flags, [loose_unicode]}. \ No newline at end of file diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index b8a58b6..c198253 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -468,35 +468,7 @@ string(<<126, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, 126)|Stack], Opts); string(<<127, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, 127)|Stack], Opts); -%% things get dumb here. erlang doesn't properly restrict unicode non-characters -%% so you can't trust the codepoints it returns always -%% the range 32..16#fdcf is safe, so allow that -string(<>, Handler, [Acc|Stack], Opts) - when ?is_noncontrol(S), S < 16#fdd0 -> - string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); -%% the range 16#fdf0..16#fffd is also safe -string(<>, Handler, [Acc|Stack], Opts) - when S > 16#fdef, S < 16#fffe -> - string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); -%% yes, i think it's insane too -string(<>, Handler, [Acc|Stack], Opts) - when S > 16#ffff andalso - S =/= 16#1fffe andalso S =/= 16#1ffff andalso - S =/= 16#2fffe andalso S =/= 16#2ffff andalso - S =/= 16#3fffe andalso S =/= 16#3ffff andalso - S =/= 16#4fffe andalso S =/= 16#4ffff andalso - S =/= 16#5fffe andalso S =/= 16#5ffff andalso - S =/= 16#6fffe andalso S =/= 16#6ffff andalso - S =/= 16#7fffe andalso S =/= 16#7ffff andalso - S =/= 16#8fffe andalso S =/= 16#8ffff andalso - S =/= 16#9fffe andalso S =/= 16#9ffff andalso - S =/= 16#afffe andalso S =/= 16#affff andalso - S =/= 16#bfffe andalso S =/= 16#bffff andalso - S =/= 16#cfffe andalso S =/= 16#cffff andalso - S =/= 16#dfffe andalso S =/= 16#dffff andalso - S =/= 16#efffe andalso S =/= 16#effff andalso - S =/= 16#ffffe andalso S =/= 16#fffff andalso - S =/= 16#10fffe andalso S =/= 16#10ffff -> +string(<>, Handler, [Acc|Stack], Opts) when ?is_noncontrol(S) -> string(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); string(Bin, Handler, Stack, Opts) -> case partial_utf(Bin) of @@ -509,35 +481,13 @@ string(Bin, Handler, Stack, Opts) -> end. %% we don't need to guard against partial utf here, because it's already taken -%% care of in string. theoretically, the last clause of noncharacter/4 is -%% unreachable -%% non-characters erlang doesn't recognize as non-characters -noncharacter(<>, Handler, [Acc|Stack], Opts) - when ?is_noncontrol(S) -> - string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); -%% u+fffe and u+ffff -noncharacter(<<239, 191, X, Rest/binary>>, Handler, [Acc|Stack], Opts) - when X == 190; X == 191 -> - string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); +%% care of in string %% surrogates noncharacter(<<237, X, _, Rest/binary>>, Handler, [Acc|Stack], Opts) when X >= 160 -> string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); -noncharacter(<>, Handler, [Acc|Stack], Opts) - when ( - (X == 240 andalso Y == 159) orelse - (X == 240 andalso Y == 175) orelse - (X == 240 andalso Y == 191) orelse - ( - (X == 241 orelse X == 242 orelse X == 243) andalso - (Y == 143 orelse Y == 159 orelse Y == 175 orelse Y == 191) - ) orelse - (X == 244 andalso Y == 143) - ) andalso (Z == 190 orelse Z == 191) -> - string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); +%% bad utf8 noncharacter(<<_, Rest/binary>>, Handler, [Acc|Stack], Opts) -> - string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); -noncharacter(Bin, Handler, Stack, Opts) -> - ?error([Bin, Handler, Stack, Opts]). + string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts). escape(<<$b, Rest/binary>>, Handler, [Acc|Stack], Opts) -> @@ -1215,7 +1165,6 @@ comments_test_() -> )} ]. - escape_forward_slash_test_() -> [ {"escape forward slash test", ?_assertEqual( @@ -1224,27 +1173,6 @@ escape_forward_slash_test_() -> )} ]. - -noncharacters_test_() -> - [ - {"noncharacters - badjson", - ?_assertEqual(check_bad(noncharacters()), []) - }, - {"noncharacters - replaced", - ?_assertEqual(check_replaced(noncharacters()), []) - } - ]. - -extended_noncharacters_test_() -> - [ - {"extended noncharacters - badjson", - ?_assertEqual(check_bad(extended_noncharacters()), []) - }, - {"extended noncharacters - replaced", - ?_assertEqual(check_replaced(extended_noncharacters()), []) - } - ]. - surrogates_test_() -> [ {"surrogates - badjson", @@ -1261,16 +1189,6 @@ control_test_() -> ?_assertEqual(check_bad(control_characters()), []) } ]. - -reserved_test_() -> - [ - {"reserved noncharacters - badjson", - ?_assertEqual(check_bad(reserved_space()), []) - }, - {"reserved noncharacters - replaced", - ?_assertEqual(check_replaced(reserved_space()), []) - } - ]. good_characters_test_() -> [ @@ -1359,34 +1277,15 @@ decode(JSON, Opts) -> catch error:badarg -> {error, badjson} end. - -noncharacters() -> lists:seq(16#fffe, 16#ffff). - -extended_noncharacters() -> - [16#1fffe, 16#1ffff, 16#2fffe, 16#2ffff] - ++ [16#3fffe, 16#3ffff, 16#4fffe, 16#4ffff] - ++ [16#5fffe, 16#5ffff, 16#6fffe, 16#6ffff] - ++ [16#7fffe, 16#7ffff, 16#8fffe, 16#8ffff] - ++ [16#9fffe, 16#9ffff, 16#afffe, 16#affff] - ++ [16#bfffe, 16#bffff, 16#cfffe, 16#cffff] - ++ [16#dfffe, 16#dffff, 16#efffe, 16#effff] - ++ [16#ffffe, 16#fffff, 16#10fffe, 16#10ffff]. - surrogates() -> lists:seq(16#d800, 16#dfff). control_characters() -> lists:seq(1, 31). -reserved_space() -> lists:seq(16#fdd0, 16#fdef). - -good() -> [32, 33] - ++ lists:seq(16#23, 16#5b) - ++ lists:seq(16#5d, 16#d7ff) - ++ lists:seq(16#e000, 16#fdcf) - ++ lists:seq(16#fdf0, 16#fffd). +good() -> [32, 33] ++ lists:seq(16#23, 16#5b) ++ lists:seq(16#5d, 16#d7ff) ++ lists:seq(16#e000, 16#ffff). -good_extended() -> lists:seq(16#100000, 16#10fffd). +good_extended() -> lists:seq(16#100000, 16#10ffff). %% erlang refuses to encode certain codepoints, so fake them all to_fake_utf(N, utf8) when N < 16#0080 -> <<34/utf8, N:8, 34/utf8>>; From 134460cf6a9488328f9ba1653e15d0f6aa2c408d Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 26 Mar 2012 19:44:41 -0700 Subject: [PATCH 66/75] update README to reflect slightly looser restrictions on what constitutes a valid string --- README.markdown | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index 707df14..3870bcd 100644 --- a/README.markdown +++ b/README.markdown @@ -76,15 +76,13 @@ when converting from erlang to json, numbers are represented with their shortest the [json spec][rfc4627] is frustratingly vague on the exact details of json strings. json must be unicode, but no encoding is specified. javascript explicitly allows strings containing codepoints explicitly disallowed by unicode. json allows implementations to set limits on the content of strings and other implementations attempt to resolve this in various ways. this implementation, in default operation, only accepts strings that meet the constraints set out in the json spec (properly escaped control characters, `"` and the escape character, `\`) and that are encoded in `utf8` -this means some codepoints that are allowed in javascript strings are not accepted by the parser. the noncharacters are specifically disallowed. the range `u+fdd0` to `u+fdef` is reserved for internal implementation use by the unicode standard and codepoints of the form `u+Xfffe` and `u+Xffff` are reserved for error detection. strings containing these codepoints are generally assumed to be invalid or improper - -also disallowed are improperly paired surrogates. `u+d800` to `u+dfff` are allowed, but only when they form valid surrogate pairs. surrogates that appear otherwise are an error +the utf8 restriction means improperly paired surrogates are explicitly disallowed. `u+d800` to `u+dfff` are allowed, but only when they form valid surrogate pairs. surrogates that appear otherwise are an error json string escapes of the form `\uXXXX` will be converted to their equivalent codepoint during parsing. this means control characters and other codepoints disallowed by the json spec may be encountered in resulting strings, but codepoints disallowed by the unicode spec (like the two cases above) will not be in the interests of pragmatism, there is an option for looser parsing, see options below -all erlang strings are represented by *valid* `utf8` encoded binaries. the encoder will check strings for conformance. the same restrictions apply as for strings encountered within json texts. that means no unpaired surrogates and no non-characters +all erlang strings are represented by *valid* `utf8` encoded binaries. the encoder will check strings for conformance. the same restrictions apply as for strings encountered within json texts. that means no unpaired surrogates this implementation performs no normalization on strings beyond that detailed here. be careful when comparing strings as equivalent strings may have different `utf8` encodings From 3d836f12412d763093624f7c536daa6b74c9181b Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Mon, 26 Mar 2012 22:10:09 -0700 Subject: [PATCH 67/75] remove debug statement from tests --- src/jsx.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsx.erl b/src/jsx.erl index d700bd5..da71bd6 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -246,7 +246,7 @@ decode(JSON, Flags) -> incremental_decode(<>, Flags) -> P = jsx_decoder:decoder(?MODULE, [], Flags ++ [explicit_end]), try incremental_decode_loop(P(C), Rest) - catch error:badarg -> io:format("~p~n", [erlang:get_stacktrace()]), {error, badjson} + catch error:badarg -> {error, badjson} end. incremental_decode_loop({incomplete, More}, <<>>) -> From 672fe04c376bb0472fa8b656b288bee426c3acac Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 13:41:23 -0700 Subject: [PATCH 68/75] additional noncharacter handling for R14BXX --- src/jsx_decoder.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index c198253..b7ebe80 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -485,6 +485,9 @@ string(Bin, Handler, Stack, Opts) -> %% surrogates noncharacter(<<237, X, _, Rest/binary>>, Handler, [Acc|Stack], Opts) when X >= 160 -> string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); +%% u+fffe and u+ffff for R14BXX +noncharacter(<<239, 191, X, Rest/binary>>, Handler, [Acc|Stack], Opts) when X == 190; X == 191 -> + string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts); %% bad utf8 noncharacter(<<_, Rest/binary>>, Handler, [Acc|Stack], Opts) -> string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts). From 42d6ef2c21805793b1aa840b1e4b9146eaf5deba Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 14:44:02 -0700 Subject: [PATCH 69/75] refactor of encoded codepoints with looser string restrictions --- priv/test_cases/escaped_noncharacter.json | 1 - priv/test_cases/escaped_noncharacter.test | 3 - priv/test_cases/escaped_noncharacter_ext.json | 1 - priv/test_cases/escaped_noncharacter_ext.test | 3 - .../escaped_noncharacter_ext_replaced.json | 1 - .../escaped_noncharacter_ext_replaced.test | 4 - .../escaped_noncharacter_replaced.json | 1 - .../escaped_noncharacter_replaced.test | 4 - priv/test_cases/escaped_reserved_a.json | 1 - priv/test_cases/escaped_reserved_a.test | 3 - priv/test_cases/escaped_reserved_b.json | 1 - priv/test_cases/escaped_reserved_b.test | 3 - src/jsx_decoder.erl | 121 ++++++++---------- 13 files changed, 50 insertions(+), 97 deletions(-) delete mode 100644 priv/test_cases/escaped_noncharacter.json delete mode 100644 priv/test_cases/escaped_noncharacter.test delete mode 100644 priv/test_cases/escaped_noncharacter_ext.json delete mode 100644 priv/test_cases/escaped_noncharacter_ext.test delete mode 100644 priv/test_cases/escaped_noncharacter_ext_replaced.json delete mode 100644 priv/test_cases/escaped_noncharacter_ext_replaced.test delete mode 100644 priv/test_cases/escaped_noncharacter_replaced.json delete mode 100644 priv/test_cases/escaped_noncharacter_replaced.test delete mode 100644 priv/test_cases/escaped_reserved_a.json delete mode 100644 priv/test_cases/escaped_reserved_a.test delete mode 100644 priv/test_cases/escaped_reserved_b.json delete mode 100644 priv/test_cases/escaped_reserved_b.test diff --git a/priv/test_cases/escaped_noncharacter.json b/priv/test_cases/escaped_noncharacter.json deleted file mode 100644 index e5c1b65..0000000 --- a/priv/test_cases/escaped_noncharacter.json +++ /dev/null @@ -1 +0,0 @@ -"\uffff" \ No newline at end of file diff --git a/priv/test_cases/escaped_noncharacter.test b/priv/test_cases/escaped_noncharacter.test deleted file mode 100644 index 4e20bc3..0000000 --- a/priv/test_cases/escaped_noncharacter.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "escaped noncharacter"}. -{jsx, {error, badjson}}. -{json, "escaped_noncharacter.json"}. \ No newline at end of file diff --git a/priv/test_cases/escaped_noncharacter_ext.json b/priv/test_cases/escaped_noncharacter_ext.json deleted file mode 100644 index f10ec2b..0000000 --- a/priv/test_cases/escaped_noncharacter_ext.json +++ /dev/null @@ -1 +0,0 @@ -"\ud83f\udfff" \ No newline at end of file diff --git a/priv/test_cases/escaped_noncharacter_ext.test b/priv/test_cases/escaped_noncharacter_ext.test deleted file mode 100644 index 7049148..0000000 --- a/priv/test_cases/escaped_noncharacter_ext.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "escaped noncharacter (extended)"}. -{jsx, {error, badjson}}. -{json, "escaped_noncharacter_ext.json"}. \ No newline at end of file diff --git a/priv/test_cases/escaped_noncharacter_ext_replaced.json b/priv/test_cases/escaped_noncharacter_ext_replaced.json deleted file mode 100644 index f10ec2b..0000000 --- a/priv/test_cases/escaped_noncharacter_ext_replaced.json +++ /dev/null @@ -1 +0,0 @@ -"\ud83f\udfff" \ No newline at end of file diff --git a/priv/test_cases/escaped_noncharacter_ext_replaced.test b/priv/test_cases/escaped_noncharacter_ext_replaced.test deleted file mode 100644 index 0a740b6..0000000 --- a/priv/test_cases/escaped_noncharacter_ext_replaced.test +++ /dev/null @@ -1,4 +0,0 @@ -{name, "escaped noncharacter (extended)"}. -{jsx, [{string, <<16#fffd/utf8>>}, end_json]}. -{json, "escaped_noncharacter_ext.json"}. -{jsx_flags, [loose_unicode]}. \ No newline at end of file diff --git a/priv/test_cases/escaped_noncharacter_replaced.json b/priv/test_cases/escaped_noncharacter_replaced.json deleted file mode 100644 index e5c1b65..0000000 --- a/priv/test_cases/escaped_noncharacter_replaced.json +++ /dev/null @@ -1 +0,0 @@ -"\uffff" \ No newline at end of file diff --git a/priv/test_cases/escaped_noncharacter_replaced.test b/priv/test_cases/escaped_noncharacter_replaced.test deleted file mode 100644 index 9c5faac..0000000 --- a/priv/test_cases/escaped_noncharacter_replaced.test +++ /dev/null @@ -1,4 +0,0 @@ -{name, "escaped noncharacter replacement"}. -{jsx, [{string,<<16#fffd/utf8>>},end_json]}. -{json, "escaped_noncharacter_replaced.json"}. -{jsx_flags, [loose_unicode]}. \ No newline at end of file diff --git a/priv/test_cases/escaped_reserved_a.json b/priv/test_cases/escaped_reserved_a.json deleted file mode 100644 index dab850b..0000000 --- a/priv/test_cases/escaped_reserved_a.json +++ /dev/null @@ -1 +0,0 @@ -"\ufdd0" \ No newline at end of file diff --git a/priv/test_cases/escaped_reserved_a.test b/priv/test_cases/escaped_reserved_a.test deleted file mode 100644 index 8a5cba2..0000000 --- a/priv/test_cases/escaped_reserved_a.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "escaped reserved a"}. -{jsx, {error, badjson}}. -{json, "escaped_reserved_a.json"}. \ No newline at end of file diff --git a/priv/test_cases/escaped_reserved_b.json b/priv/test_cases/escaped_reserved_b.json deleted file mode 100644 index be11b6e..0000000 --- a/priv/test_cases/escaped_reserved_b.json +++ /dev/null @@ -1 +0,0 @@ -"\ufdef" \ No newline at end of file diff --git a/priv/test_cases/escaped_reserved_b.test b/priv/test_cases/escaped_reserved_b.test deleted file mode 100644 index 414f024..0000000 --- a/priv/test_cases/escaped_reserved_b.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "escaped reserved b"}. -{jsx, {error, badjson}}. -{json, "escaped_reserved_b.json"}. \ No newline at end of file diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index b7ebe80..7d4faa8 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -130,6 +130,7 @@ decoder(Handler, State, Opts) -> -define(new_seq(C), [C]). -define(acc_seq(Seq, C), [C] ++ Seq). +-define(acc_seq(Seq, C, D), [C, D] ++ Seq). -define(end_seq(Seq), unicode:characters_to_binary(lists:reverse(Seq))). @@ -512,7 +513,7 @@ escape(<>, Handler, [Acc|Stack], Opts) -> escape(<>, Handler, [Acc|Stack], Opts = #opts{single_quotes=true}) -> string(Rest, Handler, [?acc_seq(Acc, ?singlequote)|Stack], Opts); escape(<<$u, Rest/binary>>, Handler, Stack, Opts) -> - escaped_unicode(Rest, Handler, [?new_seq()|Stack], Opts); + escaped_unicode(Rest, Handler, Stack, Opts); escape(<<>>, Handler, Stack, Opts) -> ?incomplete(escape, <<>>, Handler, Stack, Opts); escape(Bin, Handler, Stack, Opts) -> @@ -521,96 +522,74 @@ escape(Bin, Handler, Stack, Opts) -> %% this code is ugly and unfortunate, but so is json's handling of escaped %% unicode codepoint sequences. -escaped_unicode(<>, Handler, [[C,B,A], Acc|Stack], Opts) - when ?is_hex(D) -> +escaped_unicode(<>, Handler, [Acc|Stack], Opts) + when ?is_hex(A), ?is_hex(B), ?is_hex(C), ?is_hex(D) -> case erlang:list_to_integer([A, B, C, D], 16) of - %% high surrogate, we need a low surrogate next + %% high surrogate, dispatch to low surrogate X when X >= 16#d800, X =< 16#dbff -> low_surrogate(Rest, Handler, [X, Acc|Stack], Opts) - %% non-characters, you're not allowed to exchange these - ; X when X == 16#fffe; X == 16#ffff; X >= 16#fdd0, X =< 16#fdef -> + %% low surrogate, illegal in this position + ; X when X >= 16#dc00, X =< 16#dfff -> case Opts#opts.loose_unicode of - true -> - string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts) - ; false -> - ?error([<>, Handler, [[C,B,A], Acc|Stack], Opts]) + true -> string(Rest, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts) + ; false -> ?error([<>, Handler, [Acc|Stack], Opts]) end %% anything else - ; X -> - string(Rest, Handler, [?acc_seq(Acc, X)|Stack], Opts) + ; X -> string(Rest, Handler, [?acc_seq(Acc, X)|Stack], Opts) end; -escaped_unicode(<>, Handler, [Acc|Stack], Opts) - when ?is_hex(S) -> - escaped_unicode(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); -escaped_unicode(<<>>, Handler, Stack, Opts) -> - ?incomplete(escaped_unicode, <<>>, Handler, Stack, Opts); escaped_unicode(Bin, Handler, Stack, Opts) -> - ?error([Bin, Handler, Stack, Opts]). - - -low_surrogate(<>, Handler, Stack, Opts) -> - low_surrogate_u(Rest, Handler, Stack, Opts); -%% not an escaped codepoint, our high codepoint is illegal. dispatch back to -%% string to handle -low_surrogate(<> = Bin, Handler, [High, String|Stack], Opts) -> - case Opts#opts.loose_unicode of - true -> - string(Bin, Handler, [?acc_seq(String, 16#fffd)|Stack], Opts) - ; false -> - ?error([<>, Handler, [High, String|Stack], Opts]) - end; -low_surrogate(<<>>, Handler, Stack, Opts) -> - ?incomplete(low_surrogate, <<>>, Handler, Stack, Opts); -low_surrogate(Bin, Handler, Stack, Opts) -> - ?error([Bin, Handler, Stack, Opts]). - - -low_surrogate_u(<<$u, Rest/binary>>, Handler, Stack, Opts) -> - low_surrogate_v(Rest, Handler, [?new_seq()|Stack], Opts); -low_surrogate_u(<<>>, Handler, Stack, Opts) -> - ?incomplete(low_surrogate_u, <<>>, Handler, Stack, Opts); -%% not a low surrogate, dispatch back to string to handle, including the -%% rsolidus we parsed previously -low_surrogate_u(Bin, Handler, [High, String|Stack], Opts) -> - case Opts#opts.loose_unicode of - true -> - string(<>, Handler, [?acc_seq(String, 16#fffd)|Stack], Opts) - ; false -> - ?error([Bin, Handler, [High, String|Stack], Opts]) + case is_partial_escape(Bin) of + true -> ?incomplete(escaped_unicode, Bin, Handler, Stack, Opts) + ; false -> ?error([Bin, Handler, Stack, Opts]) end. -low_surrogate_v(<>, Handler, [[C,B,A], High, String|Stack], Opts) - when ?is_hex(D) -> +is_partial_escape(<>) when ?is_hex(A), ?is_hex(B), ?is_hex(C) -> true; +is_partial_escape(<>) when ?is_hex(A), ?is_hex(B) -> true; +is_partial_escape(<>) when ?is_hex(A) -> true; +is_partial_escape(<<>>) -> true; +is_partial_escape(_) -> false. + + +low_surrogate(<>, Handler, [High, Acc|Stack], Opts) + when ?is_hex(A), ?is_hex(B), ?is_hex(C), ?is_hex(D) -> case erlang:list_to_integer([A, B, C, D], 16) of - X when X >= 16#dc00, X =< 16#dfff -> - V = surrogate_to_codepoint(High, X), - case V rem 16#10000 of Y when Y == 16#fffe; Y == 16#ffff -> + X when X >= 16#dc00, X =< 16#dfff -> + Y = surrogate_to_codepoint(High, X), + case (Y =< 16#d800 orelse Y >= 16#e000) of + true -> string(Rest, Handler, [?acc_seq(Acc, Y)|Stack], Opts) + ; false -> case Opts#opts.loose_unicode of true -> - string(Rest, Handler, [?acc_seq(String, 16#fffd)|Stack], Opts) - ; false -> - ?error([<>, Handler, [[C,B,A], High, String|Stack], Opts]) + string(Rest, Handler, [?acc_seq(Acc, 16#fffd, 16#fffd)|Stack], Opts) + ; false -> + ?error([<>, Handler, [High, Acc|Stack], Opts]) end - ; _ -> - string(Rest, Handler, [?acc_seq(String, V)|Stack], Opts) end - %% not a low surrogate, bad bad bad ; _ -> case Opts#opts.loose_unicode of - true -> - string(Rest, Handler, [?acc_seq(?acc_seq(String, 16#fffd), 16#fffd)|Stack], Opts) - ; false -> - ?error([<>, Handler, [[C,B,A], High, String|Stack], Opts]) + true -> string(Rest, Handler, [?acc_seq(Acc, 16#fffd, 16#fffd)|Stack], Opts) + ; false -> ?error([<>, Handler, [High, Acc|Stack], Opts]) end end; -low_surrogate_v(<>, Handler, [Acc|Stack], Opts) - when ?is_hex(S) -> - low_surrogate_v(Rest, Handler, [?acc_seq(Acc, S)|Stack], Opts); -low_surrogate_v(<<>>, Handler, Stack, Opts) -> - ?incomplete(low_surrogate_v, <<>>, Handler, Stack, Opts); -low_surrogate_v(Bin, Handler, Stack, Opts) -> - ?error([Bin, Handler, Stack, Opts]). +low_surrogate(Bin, Handler, [High, Acc|Stack], Opts) -> + case is_partial_low(Bin) of + true -> ?incomplete(low_surrogate, Bin, Handler, [High, Acc|Stack], Opts) + ; false -> + case Opts#opts.loose_unicode of + true -> string(Bin, Handler, [?acc_seq(Acc, 16#fffd)|Stack], Opts) + ; false -> ?error([Bin, Handler, [High, Acc|Stack], Opts]) + end + end. + + +is_partial_low(<>) when ?is_hex(A), ?is_hex(B), ?is_hex(C) -> true; +is_partial_low(<>) when ?is_hex(A), ?is_hex(B) -> true; +is_partial_low(<>) when ?is_hex(A) -> true; +is_partial_low(<>) -> true; +is_partial_low(<>) -> true; +is_partial_low(<<>>) -> true; +is_partial_low(_) -> false. %% stole this from the unicode spec From 146c69ac1970a326fae8a8afffab225a2af28b90 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 15:52:06 -0700 Subject: [PATCH 70/75] add dmitry kolesnikov to acknowledgements --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 3870bcd..f39ffd0 100644 --- a/README.markdown +++ b/README.markdown @@ -299,7 +299,7 @@ types: ## acknowledgements ## -jsx wouldn't be what it is without the contributions of paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides, alex kropivny, steve strong and michael truog +jsx wouldn't be what it is without the contributions of paul davis, lloyd hilaiel, john engelhart, bob ippolito, fernando benavides, alex kropivny, steve strong, michael truog and dmitry kolesnikov [json]: http://json.org [yajl]: http://lloyd.github.com/yajl From 7918e63e71eb6d172bda4d1fbab33bb7e457362b Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 16:24:54 -0700 Subject: [PATCH 71/75] add empty object in array test --- priv/test_cases/empty_object_in_array.json | 1 + priv/test_cases/empty_object_in_array.test | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 priv/test_cases/empty_object_in_array.json create mode 100644 priv/test_cases/empty_object_in_array.test diff --git a/priv/test_cases/empty_object_in_array.json b/priv/test_cases/empty_object_in_array.json new file mode 100644 index 0000000..ee1aac4 --- /dev/null +++ b/priv/test_cases/empty_object_in_array.json @@ -0,0 +1 @@ +[{}] \ No newline at end of file diff --git a/priv/test_cases/empty_object_in_array.test b/priv/test_cases/empty_object_in_array.test new file mode 100644 index 0000000..0a8679d --- /dev/null +++ b/priv/test_cases/empty_object_in_array.test @@ -0,0 +1,3 @@ +{name, "empty_object_in_array"}. +{jsx, [start_array,start_object,end_object,end_array,end_json]}. +{json, "empty_object_in_array.json"}. From 4bf86a8d06bb649835c8e9bb795b785c1434e763 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 16:28:37 -0700 Subject: [PATCH 72/75] add empty string test --- priv/test_cases/empty_string.json | 1 + priv/test_cases/empty_string.test | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 priv/test_cases/empty_string.json create mode 100644 priv/test_cases/empty_string.test diff --git a/priv/test_cases/empty_string.json b/priv/test_cases/empty_string.json new file mode 100644 index 0000000..3cc762b --- /dev/null +++ b/priv/test_cases/empty_string.json @@ -0,0 +1 @@ +"" \ No newline at end of file diff --git a/priv/test_cases/empty_string.test b/priv/test_cases/empty_string.test new file mode 100644 index 0000000..c6faf71 --- /dev/null +++ b/priv/test_cases/empty_string.test @@ -0,0 +1,3 @@ +{name, "empty_string"}. +{jsx, [{string, <<>>},end_json]}. +{json, "empty_string.json"}. From 358b8f9c26cee3c043faca6baeb0844bc917626f Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 16:30:53 -0700 Subject: [PATCH 73/75] add escaped control code test --- priv/test_cases/escaped_control.json | 1 + priv/test_cases/escaped_control.test | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 priv/test_cases/escaped_control.json create mode 100644 priv/test_cases/escaped_control.test diff --git a/priv/test_cases/escaped_control.json b/priv/test_cases/escaped_control.json new file mode 100644 index 0000000..78af83f --- /dev/null +++ b/priv/test_cases/escaped_control.json @@ -0,0 +1 @@ +"\u0012" \ No newline at end of file diff --git a/priv/test_cases/escaped_control.test b/priv/test_cases/escaped_control.test new file mode 100644 index 0000000..603d719 --- /dev/null +++ b/priv/test_cases/escaped_control.test @@ -0,0 +1,3 @@ +{name, "escaped_control"}. +{jsx, [{string, <<18>>},end_json]}. +{json, "escaped_control.json"}. From 09cbbe98b5e6614dfb270236a5df48a589c52a84 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 16:33:31 -0700 Subject: [PATCH 74/75] add true, false and null tests --- priv/test_cases/false.json | 1 + priv/test_cases/false.test | 3 +++ priv/test_cases/null.json | 1 + priv/test_cases/null.test | 3 +++ priv/test_cases/true.json | 1 + priv/test_cases/true.test | 3 +++ 6 files changed, 12 insertions(+) create mode 100644 priv/test_cases/false.json create mode 100644 priv/test_cases/false.test create mode 100644 priv/test_cases/null.json create mode 100644 priv/test_cases/null.test create mode 100644 priv/test_cases/true.json create mode 100644 priv/test_cases/true.test diff --git a/priv/test_cases/false.json b/priv/test_cases/false.json new file mode 100644 index 0000000..02e4a84 --- /dev/null +++ b/priv/test_cases/false.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/priv/test_cases/false.test b/priv/test_cases/false.test new file mode 100644 index 0000000..f40af7f --- /dev/null +++ b/priv/test_cases/false.test @@ -0,0 +1,3 @@ +{name, "false"}. +{jsx, [{literal, false},end_json]}. +{json, "false.json"}. diff --git a/priv/test_cases/null.json b/priv/test_cases/null.json new file mode 100644 index 0000000..ec747fa --- /dev/null +++ b/priv/test_cases/null.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/priv/test_cases/null.test b/priv/test_cases/null.test new file mode 100644 index 0000000..ddb56d5 --- /dev/null +++ b/priv/test_cases/null.test @@ -0,0 +1,3 @@ +{name, "null"}. +{jsx, [{literal, null},end_json]}. +{json, "null.json"}. diff --git a/priv/test_cases/true.json b/priv/test_cases/true.json new file mode 100644 index 0000000..f32a580 --- /dev/null +++ b/priv/test_cases/true.json @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/priv/test_cases/true.test b/priv/test_cases/true.test new file mode 100644 index 0000000..4dfeb8c --- /dev/null +++ b/priv/test_cases/true.test @@ -0,0 +1,3 @@ +{name, "true"}. +{jsx, [{literal, true},end_json]}. +{json, "true.json"}. From 5fcada1093ff986cb316bb1045a17b1ba79f4a11 Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 27 Mar 2012 16:42:18 -0700 Subject: [PATCH 75/75] remove badly named tests --- priv/test_cases/naked_false.json | 1 - priv/test_cases/naked_false.test | 3 --- priv/test_cases/naked_null.json | 1 - priv/test_cases/naked_null.test | 3 --- priv/test_cases/naked_true.json | 1 - priv/test_cases/naked_true.test | 3 --- 6 files changed, 12 deletions(-) delete mode 100644 priv/test_cases/naked_false.json delete mode 100644 priv/test_cases/naked_false.test delete mode 100644 priv/test_cases/naked_null.json delete mode 100644 priv/test_cases/naked_null.test delete mode 100644 priv/test_cases/naked_true.json delete mode 100644 priv/test_cases/naked_true.test diff --git a/priv/test_cases/naked_false.json b/priv/test_cases/naked_false.json deleted file mode 100644 index 02e4a84..0000000 --- a/priv/test_cases/naked_false.json +++ /dev/null @@ -1 +0,0 @@ -false \ No newline at end of file diff --git a/priv/test_cases/naked_false.test b/priv/test_cases/naked_false.test deleted file mode 100644 index 5db57a1..0000000 --- a/priv/test_cases/naked_false.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "naked_false"}. -{jsx, [{literal,false},end_json]}. -{json, "naked_false.json"}. diff --git a/priv/test_cases/naked_null.json b/priv/test_cases/naked_null.json deleted file mode 100644 index ec747fa..0000000 --- a/priv/test_cases/naked_null.json +++ /dev/null @@ -1 +0,0 @@ -null \ No newline at end of file diff --git a/priv/test_cases/naked_null.test b/priv/test_cases/naked_null.test deleted file mode 100644 index 7386eaf..0000000 --- a/priv/test_cases/naked_null.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "naked_null"}. -{jsx, [{literal,null},end_json]}. -{json, "naked_null.json"}. diff --git a/priv/test_cases/naked_true.json b/priv/test_cases/naked_true.json deleted file mode 100644 index f32a580..0000000 --- a/priv/test_cases/naked_true.json +++ /dev/null @@ -1 +0,0 @@ -true \ No newline at end of file diff --git a/priv/test_cases/naked_true.test b/priv/test_cases/naked_true.test deleted file mode 100644 index 924a200..0000000 --- a/priv/test_cases/naked_true.test +++ /dev/null @@ -1,3 +0,0 @@ -{name, "naked_true"}. -{jsx, [{literal,true},end_json]}. -{json, "naked_true.json"}.