lib/borsh_decode.ex

defmodule Borsh.Decode do
  require Logger

  @moduledoc """
  The `Borsh.Decode` module provides functions for decoding data serialized using the Borsh format.
  It defines functions for each supported data type (e.g. `:string`, `:u8`, `:i64`) that can be used
  to decode a value of that type from a given bitstring.

  The primary function in this module is `borsh_decode`, which takes a bitstring and the name of a
  Borsh-serialized module as arguments. It retrieves the schema for the module and uses it to decode the
  data contained in the bitstring. The resulting data is returned as a struct of the module.

  ## Usage

  As an example let's use the following struct previuosly serialised into borsh bitstring.

  ```elixir
  defmodule ParentStruct do
   @type t() :: %__MODULE__{first_name: String.t(), last_name: String.t(), age: integer}

   defstruct [
     :first_name,
     :last_name,
     :age
   ]

   use Borsh,
     schema: [
       first_name: :string,
       last_name: :string,
       age: :u8
     ]
  end
  ```

  To use the `Borsh.Decode` module, you can pass a bitstring and the name of a
  Borsh-serialized struct to the `borsh_decode` function. For example:

  ```elixir
  bitstr = <<5, 0, 0, 0, 66, 111, 114, 105, 115, 7, 0, 0, 0, 74, 111, 104, 110, 115, 111, 110, 58>>

  Borsh.Decode.borsh_decode(bitstr, ParentStruct)
  %ParentStruct{first_name: "Boris", last_name: "Johnson", age: 58}
  ```
  """

  @doc """
  Decodes objects according to the schema into the the struct
  """
  @spec borsh_decode(bs :: bitstring(), borsh_module :: keyword) :: struct()
  def borsh_decode(bs, borsh_module) do
    borsh_schema = borsh_module.borsh_schema()

    {_, res_map} =
      Enum.map_reduce(
        borsh_schema,
        %{bits: bs, map: struct(borsh_module)},
        fn {schema_field_name, schema_field_type} = schema_item, res_map ->
          {decoded_result, rest_bits} = decode(res_map.bits, schema_field_type)

          {
            schema_item,
            %{bits: rest_bits, map: Map.put(res_map.map, schema_field_name, decoded_result)}
          }
        end
      )

    {res_map.map, res_map.bits}
  end

  # string
  defp decode(<<str_size::little-integer-size(32), rest_bits::binary>>, format)
       when format in [:string, [32], [64]] do
    <<str::binary-size(str_size), rest_bits::binary>> = rest_bits

    {str, rest_bits}
  end

  # unsigned 8-bit integer
  defp decode(<<u8::little-integer-size(8), rest_bits::binary>>, :u8) do
    {u8, rest_bits}
  end

  # unsigned 16-bit integer
  defp decode(<<u16::little-integer-size(16), rest_bits::binary>>, :u16) do
    {u16, rest_bits}
  end

  # unsigned 32-bit integer
  defp decode(<<u32::little-integer-size(32), rest_bits::binary>>, :u32) do
    {u32, rest_bits}
  end

  # unsigned 64-bit integer
  defp decode(<<u64::little-integer-size(64), rest_bits::binary>>, :u64) do
    {u64, rest_bits}
  end

  # unsigned 128-bit integer
  defp decode(<<u128::little-integer-size(128), rest_bits::binary>>, :u128) do
    {u128, rest_bits}
  end

  # signed 8-bit integer
  defp decode(<<i8::little-integer-signed-size(8), rest_bits::binary>>, :i8) do
    {i8, rest_bits}
  end

  # signed 16-bit integer
  defp decode(<<i16::little-integer-signed-size(16), rest_bits::binary>>, :i16) do
    {i16, rest_bits}
  end

  # signed 32-bit integer
  defp decode(<<i32::little-integer-signed-size(32), rest_bits::binary>>, :i32) do
    {i32, rest_bits}
  end

  # signed 64-bit integer
  defp decode(<<i64::little-integer-signed-size(64), rest_bits::binary>>, :i64) do
    {i64, rest_bits}
  end

  # signed 128-bit integer
  defp decode(<<i128::little-integer-signed-size(128), rest_bits::binary>>, :i128) do
    {i128, rest_bits}
  end

  # borsh struct
  defp decode(bits, :borsh) do
    Logger.error("Cannot decode borsh_struct: #{inspect(bits, pretty: true, limit: 30000)}")

    raise "Cannot decode borsh_struct: #{inspect(bits, pretty: true, limit: 30000)}"
  end

  # borsh struct
  defp decode(bits, {:borsh, module}) do
    module.borsh_decode(bits)
  end

  # list of borsh structs of the same type
  defp decode(<<len::little-integer-signed-size(32), bits::binary>>, [{:borsh, module}]) do
    {structs, rest_bits} =
      Enum.reduce(1..len, {[], bits}, fn _, {structs, rest} ->
        {struct, rest_bits} = decode(rest, {:borsh, module})
        {[struct | structs], rest_bits}
      end)

    {Enum.reverse(structs), rest_bits}
  end

  # list of borsh structs of any type
  defp decode(bits, l, acc \\ [])
  defp decode(bits, [], acc), do: {Enum.reverse(acc), bits}

  defp decode(<<_len::little-integer-signed-size(32), bits::binary>>, [{:borsh, module} | t], []) do
    {struct, rest_bits} = decode(bits, {:borsh, module})
    decode(rest_bits, t, [struct])
  end

  defp decode(bits, [{:borsh, module} | t], acc) do
    {struct, rest_bits} = decode(bits, {:borsh, module})
    decode(rest_bits, t, [struct | acc])
  end
end