lib/exiffer/binary.ex

defmodule Exiffer.Binary do
  @moduledoc """
  Documentation for `Exiffer.Binary`.
  """

  import Bitwise
  require Logger

  @table :exiffer

  @type rational() :: {non_neg_integer(), non_neg_integer()}
  @type signed_rational() :: {integer(), integer()}

  @spec optionally_create_ets_table() :: :ok
  def optionally_create_ets_table() do
    ref = :ets.whereis(@table)
    if ref == :undefined do
      Logger.debug "Initializing ETS table #{@table}"
      _name = :ets.new(@table, [:set, :public, :named_table])
    end
    :ok
  end

  @spec set_byte_order(:big | :little) :: true
  def set_byte_order(byte_order) do
    optionally_create_ets_table()
    :ets.insert(@table, {:byte_order, byte_order})
  end

  @spec byte_order() :: :big | :little
  def byte_order() do
    [byte_order: byte_order] = :ets.lookup(@table, :byte_order)
    byte_order
  end

  @spec to_integer(binary) :: non_neg_integer()
  @doc """
  Convert binary bytes to decimal based on endianness.
  """
  def to_integer(binary) do
    case byte_order() do
      :little -> little_endian_to_integer(binary)
      :big -> big_endian_to_integer(binary)
    end
  end

  @spec big_endian(binary) :: binary
  @doc """
  Force big endian byte order
  """
  def big_endian(binary) do
    case byte_order() do
      :little ->
        reverse(binary)
      :big ->
        binary
    end
  end

  @spec big_endian_to_current(binary) :: binary
  @doc """
  Convert big endian to the currently selected byte order
  """
  def big_endian_to_current(binary) do
    case byte_order() do
      :little ->
        reverse(binary)
      :big ->
        binary
    end
  end

  @spec int16u_to_current(integer) :: <<_::16>>
  @doc """
  Convert a 16-bit integer to binary bytes in current byte order.
  """
  def int16u_to_current(integer) do
    case byte_order() do
      :little ->
        int16u_to_little_endian(integer)
      :big ->
        int16u_to_big_endian(integer)
    end
  end

  @spec int32u_to_current(integer) :: <<_::32>>
  @doc """
  Convert a 32-bit integer to binary bytes in current byte order.
  """
  def int32u_to_current(integer) do
    case byte_order() do
      :little ->
        int32u_to_little_endian(integer)
      :big ->
        int32u_to_big_endian(integer)
    end
  end

  @spec int32s_to_current(integer()) :: <<_::32>>
  def int32s_to_current(integer) do
    case byte_order() do
      :little ->
        int32s_to_little_endian(integer)
      :big ->
        int32s_to_big_endian(integer)
    end
  end

  @spec reverse(binary) :: binary
  @doc """
  Reverse the given binary bytes
  """
  def reverse(binary) do
    binary
    |> :binary.bin_to_list()
    |> Enum.reverse()
    |> :binary.list_to_bin()
  end

  @spec big_endian_to_integer(binary) :: non_neg_integer()
  @doc """
  Convert big-endian binary bytes to an integer.
  """
  def big_endian_to_integer(<<hi, lo>>) do
    256 * hi + lo
  end

  def big_endian_to_integer(<<b0, b1, b2, b3>>) do
    0x1000000 * b0 + 0x10000 * b1 + 0x100 * b2 + b3
  end

  @spec little_endian_to_integer(binary) :: non_neg_integer()
  @doc """
  Convert little-endian binary bytes to integer.
  """
  def little_endian_to_integer(<<lo, hi>>) do
    lo + 256 * hi
  end

  def little_endian_to_integer(<<b0, b1, b2, b3>>) do
    b0 + 0x100 * b1 + 0x10000 * b2 + 0x1000000 * b3
  end

  @spec int16u_to_big_endian(integer) :: <<_::16>>
  @doc """
  Convert a 16-bit integer to big-endian binary bytes.
  """
  def int16u_to_big_endian(integer) do
    <<
    (integer &&& 0xff00) >>> 8,
    (integer &&& 0x00ff)
    >>
  end

  @spec int16u_to_little_endian(integer) :: <<_::16>>
  @doc """
  Convert a 16-bit integer to little-endian binary bytes.
  """
  def int16u_to_little_endian(integer) do
    <<
    (integer &&& 0x00ff),
    (integer &&& 0xff00) >>> 8
    >>
  end

  @spec int32u_to_big_endian(integer) :: <<_::32>>
  @doc """
  Convert a 32-bit integer to big-endian binary bytes.
  """
  def int32u_to_big_endian(integer) do
    <<
    (integer &&& 0xff000000) >>> 24,
    (integer &&& 0x00ff0000) >>> 16,
    (integer &&& 0x0000ff00) >>> 8,
    (integer &&& 0x000000ff)
    >>
  end

  @spec int32u_to_little_endian(integer) :: <<_::32>>
  @doc """
  Convert a 32-bit integer to little-endian binary bytes.
  """
  def int32u_to_little_endian(integer) do
    <<
    (integer &&& 0x000000ff),
    (integer &&& 0x0000ff00) >>> 8,
    (integer &&& 0x00ff0000) >>> 16,
    (integer &&& 0xff000000) >>> 24
    >>
  end

  @spec int32s_to_big_endian(integer) :: <<_::32>>
  def int32s_to_big_endian(integer) when integer >= 0 do
    <<
    (integer &&& 0xff000000) >>> 24,
    (integer &&& 0x00ff0000) >>> 16,
    (integer &&& 0x0000ff00) >>> 8,
    (integer &&& 0x000000ff)
    >>
  end

  def int32s_to_big_endian(integer) when integer < 0 do
    int32s_to_big_endian(0x100000000 + integer)
  end

  @spec int32s_to_little_endian(integer()) :: <<_::32>>
  def int32s_to_little_endian(integer) when integer >= 0 do
    <<
    (integer &&& 0x000000ff),
    (integer &&& 0x0000ff00) >>> 8,
    (integer &&& 0x00ff0000) >>> 16,
    (integer &&& 0xff000000) >>> 24
    >>
  end

  def int32s_to_little_endian(integer) when integer < 0 do
    int32s_to_little_endian(0x100000000 + integer)
  end

  @spec rational_to_current(rational() | [rational()]) :: binary() | [binary()]
  def rational_to_current(rationals) when is_list(rationals) do
    rationals
    |> Enum.map(&rational_to_current/1)
    |> Enum.join()
  end

  def rational_to_current({numerator, denominator}) do
    <<int32u_to_current(numerator)::binary, int32u_to_current(denominator)::binary>>
  end

  @spec rational_to_current(signed_rational() | [signed_rational()]) :: binary() | [binary()]
  def signed_rational_to_current(rationals) when is_list(rationals) do
    rationals
    |> Enum.map(&signed_rational_to_current/1)
    |> Enum.join()
  end

  def signed_rational_to_current({numerator, denominator}) do
    # TODO: handle signed values
    <<int32u_to_current(numerator)::binary, int32u_to_current(denominator)::binary>>
  end

  @spec to_signed(<<_::32>>) :: integer()
  def to_signed(<<binary::binary-size(4)>>) do
    case byte_order() do
      :little -> little_endian_to_signed(binary)
      :big -> big_endian_to_signed(binary)
    end
  end

  @spec little_endian_to_signed(<<_::32>>) :: integer()
  def little_endian_to_signed(<<b0, b1, b2, b3>>) do
    value = b0 + 0x100 * b1 + 0x10000 * b2 + 0x1000000 * b3
    negative = (b3 && 0x80) == 0x80
    if negative do
      -1 * (0x100000000 - value)
    else
      value
    end
  end

  @spec big_endian_to_signed(<<_::32>>) :: integer()
  def big_endian_to_signed(<<b0, b1, b2, b3>>) do
    value = 0x1000000 * b0 + 0x10000 * b1 + 0x100 * b2 + b3
    negative = (b0 && 0x80) == 0x80
    if negative do
      -1 * (0x100000000 - value)
    else
      value
    end
  end

  @spec to_rational(binary()) :: rational() | [rational()]
  @doc """
  When given 8 bytes, returns a single {numerator, denominator} tuple.
  When given multiples of 8 bytes, returns a list of those tuples.
  """
  def to_rational(<<numerator::binary-size(4), denominator::binary-size(4)>>) do
    {to_integer(numerator), to_integer(denominator)}
  end

  def to_rational(<<rational1::binary-size(8), rational2::binary-size(8)>>) do
    [to_rational(rational1), to_rational(rational2)]
  end

  def to_rational(<<rational::binary-size(8), rest::binary>>) do
    [to_rational(rational) | to_rational(rest)]
  end

  @spec to_signed_rational(binary()) :: signed_rational() | [signed_rational()]
  @doc """
  When given 8 bytes, returns a single {numerator, denominator} tuple.
  When given multiples of 8 bytes, returns a list of those tuples.
  """
  def to_signed_rational(<<numerator::binary-size(4), denominator::binary-size(4)>>) do
    # TODO: handle signed
    {to_integer(numerator), to_integer(denominator)}
  end

  def to_signed_rational(<<rational1::binary-size(8), rational2::binary-size(8)>>) do
    [to_signed_rational(rational1), to_signed_rational(rational2)]
  end

  def to_signed_rational(<<rational::binary-size(8), rest::binary>>) do
    [to_signed_rational(rational) | to_signed_rational(rest)]
  end
end