defmodule Jason.EncodeError do defexception [:message] @type t :: %__MODULE__{message: String.t} def new({:duplicate_key, key}) do %__MODULE__{message: "duplicate key: #{key}"} end def new({:invalid_byte, byte, original}) do %__MODULE__{message: "invalid byte #{inspect byte, base: :hex} in #{inspect original}"} end end defmodule Jason.Encode do @moduledoc """ Utilities for encoding elixir values to JSON. """ import Bitwise alias Jason.{Codegen, EncodeError, Encoder, Fragment, OrderedObject} @typep escape :: (String.t, String.t, integer -> iodata) @typep encode_map :: (map, escape, encode_map -> iodata) @opaque opts :: {escape, encode_map} @dialyzer :no_improper_lists # @compile :native @doc false @spec encode(any, map) :: {:ok, iodata} | {:error, EncodeError.t | Exception.t} def encode(value, opts) do escape = escape_function(opts) encode_map = encode_map_function(opts) try do {:ok, value(value, escape, encode_map)} catch :throw, %EncodeError{} = e -> {:error, e} :error, %Protocol.UndefinedError{protocol: Jason.Encoder} = e -> {:error, e} end end defp encode_map_function(%{maps: maps}) do case maps do :naive -> &map_naive/3 :strict -> &map_strict/3 end end defp escape_function(%{escape: escape}) do case escape do :json -> &escape_json/3 :html_safe -> &escape_html/3 :unicode_safe -> &escape_unicode/3 :javascript_safe -> &escape_javascript/3 # Keep for compatibility with Poison :javascript -> &escape_javascript/3 :unicode -> &escape_unicode/3 end end @doc """ Equivalent to calling the `Jason.Encoder.encode/2` protocol function. Slightly more efficient for built-in types because of the internal dispatching. """ @spec value(term, opts) :: iodata def value(value, {escape, encode_map}) do value(value, escape, encode_map) end @doc false # We use this directly in the helpers and deriving for extra speed def value(value, escape, _encode_map) when is_atom(value) do encode_atom(value, escape) end def value(value, escape, _encode_map) when is_binary(value) do encode_string(value, escape) end def value(value, _escape, _encode_map) when is_integer(value) do integer(value) end def value(value, _escape, _encode_map) when is_float(value) do float(value) end def value(value, escape, encode_map) when is_list(value) do list(value, escape, encode_map) end def value(%{__struct__: module} = value, escape, encode_map) do struct(value, escape, encode_map, module) end def value(value, escape, encode_map) when is_map(value) do case Map.to_list(value) do [] -> "{}" keyword -> encode_map.(keyword, escape, encode_map) end end def value(value, escape, encode_map) do Encoder.encode(value, {escape, encode_map}) end @compile {:inline, integer: 1, float: 1} @spec atom(atom, opts) :: iodata def atom(atom, {escape, _encode_map}) do encode_atom(atom, escape) end defp encode_atom(nil, _escape), do: "null" defp encode_atom(true, _escape), do: "true" defp encode_atom(false, _escape), do: "false" defp encode_atom(atom, escape), do: encode_string(Atom.to_string(atom), escape) @spec integer(integer) :: iodata def integer(integer) do Integer.to_string(integer) end has_short_format = try do :erlang.float_to_binary(1.0, [:short]) catch _, _ -> false else _ -> true end @spec float(float) :: iodata if has_short_format do def float(float) do :erlang.float_to_binary(float, [:short]) end else def float(float) do :io_lib_format.fwrite_g(float) end end @spec list(list, opts) :: iodata def list(list, {escape, encode_map}) do list(list, escape, encode_map) end defp list([], _escape, _encode_map) do "[]" end defp list([head | tail], escape, encode_map) do [?[, value(head, escape, encode_map) | list_loop(tail, escape, encode_map)] end defp list_loop([], _escape, _encode_map) do ']' end defp list_loop([head | tail], escape, encode_map) do [?,, value(head, escape, encode_map) | list_loop(tail, escape, encode_map)] end @spec keyword(keyword, opts) :: iodata def keyword(list, _) when list == [], do: "{}" def keyword(list, {escape, encode_map}) when is_list(list) do encode_map.(list, escape, encode_map) end @spec map(map, opts) :: iodata def map(value, {escape, encode_map}) do case Map.to_list(value) do [] -> "{}" keyword -> encode_map.(keyword, escape, encode_map) end end defp map_naive([{key, value} | tail], escape, encode_map) do ["{\"", key(key, escape), "\":", value(value, escape, encode_map) | map_naive_loop(tail, escape, encode_map)] end defp map_naive_loop([], _escape, _encode_map) do '}' end defp map_naive_loop([{key, value} | tail], escape, encode_map) do [",\"", key(key, escape), "\":", value(value, escape, encode_map) | map_naive_loop(tail, escape, encode_map)] end defp map_strict([{key, value} | tail], escape, encode_map) do key = IO.iodata_to_binary(key(key, escape)) visited = %{key => []} ["{\"", key, "\":", value(value, escape, encode_map) | map_strict_loop(tail, escape, encode_map, visited)] end defp map_strict_loop([], _encode_map, _escape, _visited) do '}' end defp map_strict_loop([{key, value} | tail], escape, encode_map, visited) do key = IO.iodata_to_binary(key(key, escape)) case visited do %{^key => _} -> error({:duplicate_key, key}) _ -> visited = Map.put(visited, key, []) [",\"", key, "\":", value(value, escape, encode_map) | map_strict_loop(tail, escape, encode_map, visited)] end end @spec struct(struct, opts) :: iodata def struct(%module{} = value, {escape, encode_map}) do struct(value, escape, encode_map, module) end # TODO: benchmark the effect of inlining the to_iso8601 functions for module <- [Date, Time, NaiveDateTime, DateTime] do defp struct(value, _escape, _encode_map, unquote(module)) do [?", unquote(module).to_iso8601(value), ?"] end end defp struct(value, _escape, _encode_map, Decimal) do # silence the xref warning decimal = Decimal [?", decimal.to_string(value, :normal), ?"] end defp struct(value, escape, encode_map, Fragment) do %{encode: encode} = value encode.({escape, encode_map}) end defp struct(value, escape, encode_map, OrderedObject) do case value do %{values: []} -> "{}" %{values: values} -> encode_map.(values, escape, encode_map) end end defp struct(value, escape, encode_map, _module) do Encoder.encode(value, {escape, encode_map}) end @doc false # This is used in the helpers and deriving implementation def key(string, escape) when is_binary(string) do escape.(string, string, 0) end def key(atom, escape) when is_atom(atom) do string = Atom.to_string(atom) escape.(string, string, 0) end def key(other, escape) do string = String.Chars.to_string(other) escape.(string, string, 0) end @spec string(String.t, opts) :: iodata def string(string, {escape, _encode_map}) do encode_string(string, escape) end defp encode_string(string, escape) do [?", escape.(string, string, 0), ?"] end slash_escapes = Enum.zip('\b\t\n\f\r\"\\', 'btnfr"\\') surogate_escapes = Enum.zip([0x2028, 0x2029], ["\\u2028", "\\u2029"]) ranges = [{0x00..0x1F, :unicode} | slash_escapes] html_ranges = [{0x00..0x1F, :unicode}, {?<, :unicode}, {?/, ?/} | slash_escapes] escape_jt = Codegen.jump_table(html_ranges, :error) Enum.each(escape_jt, fn {byte, :unicode} -> sequence = List.to_string(:io_lib.format("\\u~4.16.0B", [byte])) defp escape(unquote(byte)), do: unquote(sequence) {byte, char} when is_integer(char) -> defp escape(unquote(byte)), do: unquote(<>) {byte, :error} -> defp escape(unquote(byte)), do: throw(:error) end) ## regular JSON escape json_jt = Codegen.jump_table(ranges, :chunk, 0x7F + 1) defp escape_json(data, original, skip) do escape_json(data, [], original, skip) end Enum.map(json_jt, fn {byte, :chunk} -> defp escape_json(<>, acc, original, skip) when byte === unquote(byte) do escape_json_chunk(rest, acc, original, skip, 1) end {byte, _escape} -> defp escape_json(<>, acc, original, skip) when byte === unquote(byte) do acc = [acc | escape(byte)] escape_json(rest, acc, original, skip + 1) end end) defp escape_json(<>, acc, original, skip) when char <= 0x7FF do escape_json_chunk(rest, acc, original, skip, 2) end defp escape_json(<>, acc, original, skip) when char <= 0xFFFF do escape_json_chunk(rest, acc, original, skip, 3) end defp escape_json(<<_char::utf8, rest::bits>>, acc, original, skip) do escape_json_chunk(rest, acc, original, skip, 4) end defp escape_json(<<>>, acc, _original, _skip) do acc end defp escape_json(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end Enum.map(json_jt, fn {byte, :chunk} -> defp escape_json_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do escape_json_chunk(rest, acc, original, skip, len + 1) end {byte, _escape} -> defp escape_json_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do part = binary_part(original, skip, len) acc = [acc, part | escape(byte)] escape_json(rest, acc, original, skip + len + 1) end end) defp escape_json_chunk(<>, acc, original, skip, len) when char <= 0x7FF do escape_json_chunk(rest, acc, original, skip, len + 2) end defp escape_json_chunk(<>, acc, original, skip, len) when char <= 0xFFFF do escape_json_chunk(rest, acc, original, skip, len + 3) end defp escape_json_chunk(<<_char::utf8, rest::bits>>, acc, original, skip, len) do escape_json_chunk(rest, acc, original, skip, len + 4) end defp escape_json_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end defp escape_json_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end ## javascript safe JSON escape defp escape_javascript(data, original, skip) do escape_javascript(data, [], original, skip) end Enum.map(json_jt, fn {byte, :chunk} -> defp escape_javascript(<>, acc, original, skip) when byte === unquote(byte) do escape_javascript_chunk(rest, acc, original, skip, 1) end {byte, _escape} -> defp escape_javascript(<>, acc, original, skip) when byte === unquote(byte) do acc = [acc | escape(byte)] escape_javascript(rest, acc, original, skip + 1) end end) defp escape_javascript(<>, acc, original, skip) when char <= 0x7FF do escape_javascript_chunk(rest, acc, original, skip, 2) end Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_javascript(<>, acc, original, skip) do acc = [acc | unquote(escape)] escape_javascript(rest, acc, original, skip + 3) end end) defp escape_javascript(<>, acc, original, skip) when char <= 0xFFFF do escape_javascript_chunk(rest, acc, original, skip, 3) end defp escape_javascript(<<_char::utf8, rest::bits>>, acc, original, skip) do escape_javascript_chunk(rest, acc, original, skip, 4) end defp escape_javascript(<<>>, acc, _original, _skip) do acc end defp escape_javascript(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end Enum.map(json_jt, fn {byte, :chunk} -> defp escape_javascript_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do escape_javascript_chunk(rest, acc, original, skip, len + 1) end {byte, _escape} -> defp escape_javascript_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do part = binary_part(original, skip, len) acc = [acc, part | escape(byte)] escape_javascript(rest, acc, original, skip + len + 1) end end) defp escape_javascript_chunk(<>, acc, original, skip, len) when char <= 0x7FF do escape_javascript_chunk(rest, acc, original, skip, len + 2) end Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_javascript_chunk(<>, acc, original, skip, len) do part = binary_part(original, skip, len) acc = [acc, part | unquote(escape)] escape_javascript(rest, acc, original, skip + len + 3) end end) defp escape_javascript_chunk(<>, acc, original, skip, len) when char <= 0xFFFF do escape_javascript_chunk(rest, acc, original, skip, len + 3) end defp escape_javascript_chunk(<<_char::utf8, rest::bits>>, acc, original, skip, len) do escape_javascript_chunk(rest, acc, original, skip, len + 4) end defp escape_javascript_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end defp escape_javascript_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end ## HTML safe JSON escape html_jt = Codegen.jump_table(html_ranges, :chunk, 0x7F + 1) defp escape_html(data, original, skip) do escape_html(data, [], original, skip) end Enum.map(html_jt, fn {byte, :chunk} -> defp escape_html(<>, acc, original, skip) when byte === unquote(byte) do escape_html_chunk(rest, acc, original, skip, 1) end {byte, _escape} -> defp escape_html(<>, acc, original, skip) when byte === unquote(byte) do acc = [acc | escape(byte)] escape_html(rest, acc, original, skip + 1) end end) defp escape_html(<>, acc, original, skip) when char <= 0x7FF do escape_html_chunk(rest, acc, original, skip, 2) end Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_html(<>, acc, original, skip) do acc = [acc | unquote(escape)] escape_html(rest, acc, original, skip + 3) end end) defp escape_html(<>, acc, original, skip) when char <= 0xFFFF do escape_html_chunk(rest, acc, original, skip, 3) end defp escape_html(<<_char::utf8, rest::bits>>, acc, original, skip) do escape_html_chunk(rest, acc, original, skip, 4) end defp escape_html(<<>>, acc, _original, _skip) do acc end defp escape_html(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end Enum.map(html_jt, fn {byte, :chunk} -> defp escape_html_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do escape_html_chunk(rest, acc, original, skip, len + 1) end {byte, _escape} -> defp escape_html_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do part = binary_part(original, skip, len) acc = [acc, part | escape(byte)] escape_html(rest, acc, original, skip + len + 1) end end) defp escape_html_chunk(<>, acc, original, skip, len) when char <= 0x7FF do escape_html_chunk(rest, acc, original, skip, len + 2) end Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_html_chunk(<>, acc, original, skip, len) do part = binary_part(original, skip, len) acc = [acc, part | unquote(escape)] escape_html(rest, acc, original, skip + len + 3) end end) defp escape_html_chunk(<>, acc, original, skip, len) when char <= 0xFFFF do escape_html_chunk(rest, acc, original, skip, len + 3) end defp escape_html_chunk(<<_char::utf8, rest::bits>>, acc, original, skip, len) do escape_html_chunk(rest, acc, original, skip, len + 4) end defp escape_html_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end defp escape_html_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end ## unicode escape defp escape_unicode(data, original, skip) do escape_unicode(data, [], original, skip) end Enum.map(json_jt, fn {byte, :chunk} -> defp escape_unicode(<>, acc, original, skip) when byte === unquote(byte) do escape_unicode_chunk(rest, acc, original, skip, 1) end {byte, _escape} -> defp escape_unicode(<>, acc, original, skip) when byte === unquote(byte) do acc = [acc | escape(byte)] escape_unicode(rest, acc, original, skip + 1) end end) defp escape_unicode(<>, acc, original, skip) when char <= 0xFF do acc = [acc, "\\u00" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 2) end defp escape_unicode(<>, acc, original, skip) when char <= 0x7FF do acc = [acc, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 2) end defp escape_unicode(<>, acc, original, skip) when char <= 0xFFF do acc = [acc, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 3) end defp escape_unicode(<>, acc, original, skip) when char <= 0xFFFF do acc = [acc, "\\u" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 3) end defp escape_unicode(<>, acc, original, skip) do char = char - 0x10000 acc = [ acc, "\\uD", Integer.to_string(0x800 ||| (char >>> 10), 16), "\\uD" | Integer.to_string(0xC00 ||| (char &&& 0x3FF), 16) ] escape_unicode(rest, acc, original, skip + 4) end defp escape_unicode(<<>>, acc, _original, _skip) do acc end defp escape_unicode(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end Enum.map(json_jt, fn {byte, :chunk} -> defp escape_unicode_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do escape_unicode_chunk(rest, acc, original, skip, len + 1) end {byte, _escape} -> defp escape_unicode_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do part = binary_part(original, skip, len) acc = [acc, part | escape(byte)] escape_unicode(rest, acc, original, skip + len + 1) end end) defp escape_unicode_chunk(<>, acc, original, skip, len) when char <= 0xFF do part = binary_part(original, skip, len) acc = [acc, part, "\\u00" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 2) end defp escape_unicode_chunk(<>, acc, original, skip, len) when char <= 0x7FF do part = binary_part(original, skip, len) acc = [acc, part, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 2) end defp escape_unicode_chunk(<>, acc, original, skip, len) when char <= 0xFFF do part = binary_part(original, skip, len) acc = [acc, part, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 3) end defp escape_unicode_chunk(<>, acc, original, skip, len) when char <= 0xFFFF do part = binary_part(original, skip, len) acc = [acc, part, "\\u" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 3) end defp escape_unicode_chunk(<>, acc, original, skip, len) do char = char - 0x10000 part = binary_part(original, skip, len) acc = [ acc, part, "\\uD", Integer.to_string(0x800 ||| (char >>> 10), 16), "\\uD" | Integer.to_string(0xC00 ||| (char &&& 0x3FF), 16) ] escape_unicode(rest, acc, original, skip + len + 4) end defp escape_unicode_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end defp escape_unicode_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end @compile {:inline, error: 1} defp error(error) do throw EncodeError.new(error) end end