Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#408 #433

Merged
merged 10 commits into from
Jul 21, 2022
67 changes: 67 additions & 0 deletions lib/archethic/utils/varint.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule Archethic.Utils.VarInt do
@moduledoc """
VarInt is a Module for support of multi-byte length integers
"""

defstruct [:bytes, :value]

@type t :: %__MODULE__{
bytes: non_neg_integer(),
value: non_neg_integer()
}

def from_value(value) do
prix-uniris marked this conversation as resolved.
Show resolved Hide resolved
bytes = value |> min_bytes_to_store()

%__MODULE__{
bytes: bytes,
value: value
}
end

def from_map(varint = %{}) do
prix-uniris marked this conversation as resolved.
Show resolved Hide resolved
%__MODULE__{
bytes: Map.get(varint, :bytes),
value: Map.get(varint, :value)
}
end

@spec min_bytes_to_store(integer()) :: integer()
defp min_bytes_to_store(value) do
# Since values go from
# 1*8 => 2^8 => 256
# 2*8 => 16 => 2^16 => 65536
# 3*8 => 24 => 2^24 => 16777216
ranges =
1..256
prix-uniris marked this conversation as resolved.
Show resolved Hide resolved
|> Enum.with_index(fn element, index -> {index + 1, element} end)
|> Enum.map(fn {i, x} -> {i, Integer.pow(2, 8 * x)} end)

# Since Range is in sorted order, first find would be the least amount of bytes required range.
{bytes, _range} = ranges |> Enum.find(fn {_bytes, range_max} -> value < range_max end)
prix-uniris marked this conversation as resolved.
Show resolved Hide resolved
bytes
end

@spec serialize(__MODULE__.t()) :: <<_::64, _::_*8>>
prix-uniris marked this conversation as resolved.
Show resolved Hide resolved
def serialize(%__MODULE__{bytes: bytes, value: value}) do
<<bytes::8>> <> <<value::size(bytes)-unit(8)>>
prix-uniris marked this conversation as resolved.
Show resolved Hide resolved
end

@spec deserialize(bitstring()) :: __MODULE__.t()
def deserialize(data) do
<<bytes::8, rest::bitstring>> = data

if byte_size(rest) != bytes do
raise ArgumentError,
message:
"the argument value is invalid, Byte Size Supplied: #{bytes}, Bytes found : #{byte_size(rest)}. Should be equal."
end

<<value::size(bytes)-unit(8)>> = rest

%__MODULE__{
bytes: bytes,
value: value
}
prix-uniris marked this conversation as resolved.
Show resolved Hide resolved
end
end
40 changes: 40 additions & 0 deletions test/archethic/utils/varint.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule VarIntTest do
use ExUnit.Case
alias Archethic.Utils.VarInt

doctest VarInt

test "should encode 8 bit number" do
assert VarInt.from_value(25) |> VarInt.serialize() == <<1::8, 25::8>>
end

test "should deserialize 8 bit encoded bitstring" do
data = <<3, 2, 184, 169>>
assert %VarInt{bytes: 3, value: 178_345} == data |> VarInt.deserialize()
end

test "should encode and decode randomly 100 integers" do
numbers =
1..100
|> Enum.map(fn x -> Integer.pow(1..2048 |> Enum.random(), x) end)

# Encode the numbers in structs
struct_nums = numbers |> Enum.map(fn x -> x |> VarInt.from_value() end)

# Serialize the numbers in bitstring
serialized_numbers = struct_nums |> Enum.map(fn x -> x |> VarInt.serialize() end)

# Deserialize the bitstrings to struct
decoded_numbers = serialized_numbers |> Enum.map(fn x -> x |> VarInt.deserialize() end)

assert length(struct_nums -- decoded_numbers) == 0
end

test "Should Raise an Error on Malformed Argument Supplied" do
data = <<2, 34>>

assert_raise ArgumentError, fn ->
data |> VarInt.deserialize()
end
end
end