#
# 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
@type bson_type ::
float()
| String.t()
| bson_map()
| list(bson_type())
| {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 <<doc_size::little-32, rest::binary>> 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),
{<<type::8, key::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, <<value::little-float-64, rest::binary>>) do
Map.put(map, key, value)
|> parse_doc_bytes(rest)
end
defp parse_value(0x2, map, key, <<string_size::little-32, string_and_rest::binary>>) do
with no_zero_size when no_zero_size >= 0 <- string_size - 1,
<<value::binary-size(no_zero_size), 0::8, rest::binary>> <- 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, <<subdoc_size::little-32, subdoc_and_rest::binary>>) do
with the_size when the_size >= 1 <- subdoc_size - 4,
<<subdocument::binary-size(the_size), rest::binary>> <- 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, <<subdoc_size::little-32, subdoc_and_rest::binary>>) do
with the_size when the_size >= 1 <- subdoc_size - 4,
<<subdocument::binary-size(the_size), rest::binary>> <- 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, <<subdoc_size::little-32, subtype::8, subdoc_and_rest::binary>>) do
with the_size when the_size >= 0 <- subdoc_size,
<<subdocument::binary-size(the_size), rest::binary>> <- subdoc_and_rest do
Map.put(map, key, {subtype, 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, <<value::signed-little-64, rest::binary>>) 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, <<value::signed-little-32, rest::binary>>) do
Map.put(map, key, value)
|> parse_doc_bytes(rest)
end
defp parse_value(0x12, map, key, <<value::signed-little-64, rest::binary>>) 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::binary-size(n), 0::8, rest::binary>> ->
{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(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
[<<doc_size::signed-little-32>>, document, <<0>>]
end
end
defp encode_value(key_string, value) when is_float(value) do
[<<0x1>>, key_string, <<0>> | <<value::little-float-64>>]
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, %DateTime{} = value) do
timestamp_ms = DateTime.to_unix(value, :millisecond)
[<<0x9>>, key_string, <<0>> | <<timestamp_ms::signed-little-64>>]
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
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>> | <<value::signed-little-32>>]
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>> | <<value::signed-little-64>>]
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