# # This file is part of Cyanide. # # Copyright 2019 Ispirata Srl # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # defmodule Cyanide do alias Cyanide.Binary alias Cyanide.EncodedValue alias Cyanide.Encoder @type bson_type :: float() | String.t() | bson_map() | list(bson_type()) | Binary.t() | {integer(), binary()} | boolean() | nil | integer() | DateTime.t() @type bson_map :: %{optional(String.t()) => bson_type()} @type encodable_map_key :: atom() | String.t() @type encodable_map :: %{encodable_map_key() => bson_type()} @spec decode(binary()) :: {:ok, bson_map()} | {:error, :invalid_bson} def decode(document) do with <> when doc_size == byte_size(rest) + 4 <- document, values_map when is_map(values_map) <- parse_doc_bytes(%{}, rest) do {:ok, values_map} else wrong_size when is_integer(wrong_size) -> {:error, :invalid_bson} short_document when is_binary(short_document) -> {:error, :invalid_bson} {:error, :invalid_bson} -> {:error, :invalid_bson} end end @spec decode!(binary) :: bson_map def decode!(document) do {:ok, document_map} = decode(document) document_map end defp parse_doc_bytes(map, <<0>>) do map end defp parse_doc_bytes(map, key_value_binary) do with splitted = split_cstring(key_value_binary), {<>, rest} <- splitted, true <- String.valid?(key) do parse_value(type, map, key, rest) else _any -> {:error, :invalid_bson} end end defp parse_value(0x1, map, key, <<0::48, 240, 127, rest::binary>>) do Map.put(map, key, :inf) |> parse_doc_bytes(rest) end defp parse_value(0x1, map, key, <<0::48, 240, 255, rest::binary>>) do Map.put(map, key, :"-inf") |> parse_doc_bytes(rest) end defp parse_value(0x1, map, key, <<0::48, 248, 127, rest::binary>>) do Map.put(map, key, :NaN) |> parse_doc_bytes(rest) end defp parse_value(0x1, map, key, <<0::48, 248, 255, rest::binary>>) do Map.put(map, key, :NaN) |> parse_doc_bytes(rest) end defp parse_value(0x1, map, key, <>) do Map.put(map, key, value) |> parse_doc_bytes(rest) end defp parse_value(0x2, map, key, <>) do with no_zero_size when no_zero_size >= 0 <- string_size - 1, <> <- string_and_rest do Map.put(map, key, value) |> parse_doc_bytes(rest) else _any -> {:error, :invalid_bson} end end defp parse_value(0x3, map, key, <>) do with the_size when the_size >= 1 <- subdoc_size - 4, <> <- subdoc_and_rest do Map.put(map, key, parse_doc_bytes(%{}, subdocument)) |> parse_doc_bytes(rest) else _any -> {:error, :invalid_bson} end end defp parse_value(0x4, map, key, <<5, 0, 0, 0, 0, rest::binary>>) do Map.put(map, key, []) |> parse_doc_bytes(rest) end defp parse_value(0x4, map, key, <>) do with the_size when the_size >= 1 <- subdoc_size - 4, <> <- subdoc_and_rest, array_subdoc when is_map(array_subdoc) <- parse_doc_bytes(%{}, subdocument) do array_max_index = map_size(array_subdoc) - 1 map_array_to_list = fn index, acc -> with index_string = to_string(index), {:ok, value} <- Map.fetch(array_subdoc, index_string) do {:cont, [value | acc]} else :error -> {:halt, {:error, :invalid_bson}} end end with values_list when is_list(values_list) <- Enum.reduce_while(array_max_index..0, [], map_array_to_list) do Map.put(map, key, values_list) |> parse_doc_bytes(rest) end else _any -> {:error, :invalid_bson} end end defp parse_value(0x5, map, key, <>) do with the_size when the_size >= 0 <- subdoc_size, <> <- subdoc_and_rest, {:ok, subtype_atom} <- Binary.cast_subtype(subtype) do Map.put(map, key, %Binary{subtype: subtype_atom, data: subdocument}) |> parse_doc_bytes(rest) else _any -> {:error, :invalid_bson} end end defp parse_value(0x8, map, key, <<0::8, rest::binary>>) do Map.put(map, key, false) |> parse_doc_bytes(rest) end defp parse_value(0x8, map, key, <<1::8, rest::binary>>) do Map.put(map, key, true) |> parse_doc_bytes(rest) end defp parse_value(0x9, map, key, <>) do with {:ok, datetime} <- DateTime.from_unix(value, :millisecond) do Map.put(map, key, datetime) |> parse_doc_bytes(rest) else _any -> {:error, :invalid_bson} end end defp parse_value(0xA, map, key, rest) do Map.put(map, key, nil) |> parse_doc_bytes(rest) end defp parse_value(0x10, map, key, <>) do Map.put(map, key, value) |> parse_doc_bytes(rest) end defp parse_value(0x12, map, key, <>) do Map.put(map, key, value) |> parse_doc_bytes(rest) end defp parse_value(_type, _map, _key, _invalid_bson) do {:error, :invalid_bson} end defp split_cstring(blob) do split_cstring(blob, 1, byte_size(blob) - 1) end defp split_cstring(blob, n, max_len) when n < max_len do case blob do <> -> {cstring, rest} _ -> split_cstring(blob, n + 1, max_len) end end defp split_cstring(_blob, _n, _max_len) do :error end @spec encode(encodable_map()) :: {:ok, binary()} | {:error, :cannot_bson_encode} def encode(document) do with {:ok, doc_iolist} <- document_to_iolist(document) do {:ok, :erlang.iolist_to_binary(doc_iolist)} else :error -> {:error, :cannot_bson_encode} end end @spec encode!(encodable_map()) :: binary() def encode!(document) do {:ok, document_binary} = encode(document) document_binary end defp document_to_iolist(%_{} = struct) do case Encoder.encode(struct) do document when is_map(document) -> document_to_iolist(document) _ -> :error end end defp document_to_iolist(document) do values_io_list = Enum.map(document, fn {key, value} -> to_string(key) |> encode_value(value) end) with document_iolist when is_list(document_iolist) <- finalize_document(values_io_list) do {:ok, document_iolist} end end defp finalize_document(document) do if Enum.any?(document, fn item -> item == :error end) do :error else doc_size = :erlang.iolist_size(document) + 5 [<>, document, <<0>>] end end defp encode_value(key_string, value) when is_float(value) do [<<0x1>>, key_string, <<0>> | <>] end defp encode_value(key_string, :NaN) do [<<0x1>>, key_string, <<0, 0::48, 248, 127>>] end defp encode_value(key_string, :inf) do [<<0x1>>, key_string, <<0, 0::48, 240, 127>>] end defp encode_value(key_string, :"-inf") do [<<0x1>>, key_string, <<0, 0::48, 240, 255>>] end defp encode_value(key_string, value) when is_binary(value) do string_size = byte_size(value) + 1 [<<0x2>>, key_string, <<0, string_size::signed-little-32>>, value | <<0>>] end defp encode_value(key_string, %Binary{subtype: subtype, data: data}) do subtype_int = case subtype do :generic -> 0x00 :function -> 0x01 :old_binary -> 0x02 :old_uuid -> 0x03 :uuid -> 0x04 :md5 -> 0x05 :encrypted_bson -> 0x06 ud when is_integer(ud) and ud >= 0x80 and ud <= 0xFF -> ud end encode_value(key_string, {subtype_int, data}) end defp encode_value(key_string, %DateTime{} = value) do timestamp_ms = DateTime.to_unix(value, :millisecond) [<<0x9>>, key_string, <<0>> | <>] end defp encode_value(key_string, %_{} = encodable) do case Encoder.encode(encodable) do %EncodedValue{tag: tag, data: data} -> [<>, key_string, <<0::8, data::binary>>] any -> encode_value(key_string, any) end end defp encode_value(key_string, value) when is_map(value) do with {:ok, doc_iolist} <- document_to_iolist(value) do [<<0x3>>, key_string, <<0>> | doc_iolist] end end defp encode_value(key_string, value) when is_list(value) do with the_list when is_list(the_list) <- reverse_encode_list(value, 0, []) do [<<0x4>>, key_string, <<0>> | the_list] end end @deprecated "use %Cyanide.Binary{} instead" defp encode_value(key_string, {subtype, value}) when is_integer(subtype) and subtype >= 0 and subtype <= 255 and is_binary(value) do binary_size = byte_size(value) [<<0x5>>, key_string, <<0, binary_size::signed-little-32, subtype::8>>, value] end defp encode_value(key_string, false) do [<<0x8>>, key_string | <<0, 0>>] end defp encode_value(key_string, true) do [<<0x8>>, key_string | <<0, 1>>] end defp encode_value(key_string, nil) do [<<0xA>>, key_string | <<0>>] end defp encode_value(key_string, value) when is_integer(value) and value >= -2_147_483_648 and value <= 2_147_483_647 do [<<0x10>>, key_string, <<0>> | <>] end defp encode_value(key_string, value) when is_integer(value) and value >= -9_223_372_036_854_775_808 and value <= 9_223_372_036_854_775_807 do [<<0x12>>, key_string, <<0>> | <>] end defp encode_value(_key_string, _value) do :error end defp reverse_encode_list(_list, _index, [:error | _t]) do :error end defp reverse_encode_list([], _index, acc_list) do reversed_list = :lists.reverse(acc_list) finalize_document(reversed_list) end defp reverse_encode_list([head_value | tail], index, acc_list) do reverse_encode_list(tail, index + 1, [ encode_value(Integer.to_string(index), head_value) | acc_list ]) end end