defmodule Hobbes.Encoding.Keyset do @moduledoc """ Encodings for Hobbes keys and values. """ @escape 0xFF @nil_value 0x00 @binary 0x01 @nested_list 0x05 @int_neg_8 0x0C @int_neg_7 0x0D @int_neg_6 0x0E @int_neg_5 0x0F @int_neg_4 0x10 @int_neg_3 0x11 @int_neg_2 0x12 @int_neg_1 0x13 @int_0 0x14 @int_pos_1 0x15 @int_pos_2 0x16 @int_pos_3 0x17 @int_pos_4 0x18 @int_pos_5 0x1A @int_pos_6 0x1B @int_pos_7 0x1C @int_pos_8 0x1D @bool_false 0x26 @bool_true 0x27 @spec pack(list) :: binary def pack(list) when is_list(list) do Enum.map(list, &encode(&1, 0)) |> IO.iodata_to_binary() end def pack(term) do raise """ Keyset.pack/1 expects a top-level list, got: #{inspect(term)} If you want to encode a single term, wrap it in a list, like: Keyset.pack([123]) """ end @spec unpack(binary) :: list def unpack(binary) when is_binary(binary) do case decode(binary, 0) do {values, ""} -> values end end defp encode(nil, 0), do: <<@nil_value>> defp encode(nil, depth) when depth > 0, do: <<@nil_value, @escape>> defp encode(false, _depth), do: <<@bool_false>> defp encode(true, _depth), do: <<@bool_true>> defp encode(list, depth) when is_list(list) do values = Enum.map(list, &encode(&1, depth + 1)) [@nested_list, values, @nil_value] end defp encode(binary, _depth) when is_binary(binary) do [<<@binary>>, encode_binary(binary, 0)] end defp encode(int, _depth) when is_integer(int) and int < 0 do binary = :binary.encode_unsigned(-int) binary = for <>, into: "", do: <> case byte_size(binary) do 8 -> [<<@int_neg_8>>, binary] 7 -> [<<@int_neg_7>>, binary] 6 -> [<<@int_neg_6>>, binary] 5 -> [<<@int_neg_5>>, binary] 4 -> [<<@int_neg_4>>, binary] 3 -> [<<@int_neg_3>>, binary] 2 -> [<<@int_neg_2>>, binary] 1 -> [<<@int_neg_1>>, binary] end end defp encode(0, _depth) do <<@int_0>> end defp encode(int, _depth) when is_integer(int) and int > 0 do binary = :binary.encode_unsigned(int) case byte_size(binary) do 1 -> [<<@int_pos_1>>, binary] 2 -> [<<@int_pos_2>>, binary] 3 -> [<<@int_pos_3>>, binary] 4 -> [<<@int_pos_4>>, binary] 5 -> [<<@int_pos_5>>, binary] 6 -> [<<@int_pos_6>>, binary] 7 -> [<<@int_pos_7>>, binary] 8 -> [<<@int_pos_8>>, binary] end end # Encodes a null-terminated binary, nulls are escaped as <<0x00, 0xFF>> defp encode_binary(binary, offset) do case binary do <> -> # Terminate encoded binary (null-terminated) [head, <<@nil_value>>] <> -> # Escape null value in binary and keep going [head, <<@nil_value, @escape>> | encode_binary(tail, 0)] <<_head::binary-size(offset), _other, _tail::binary>> -> # Non-null character, keep scanning encode_binary(binary, offset + 1) end end defp decode("", 0) do {[], ""} end defp decode(<<@nil_value, rest::binary>>, 0 = depth) do {values, tail} = decode(rest, depth) {[nil | values], tail} end # Escaped nil within a nested list defp decode(<<@nil_value, @escape, rest::binary>>, depth) when depth > 0 do {values, tail} = decode(rest, depth) {[nil | values], tail} end # Closing a nested list defp decode(<<@nil_value, rest::binary>>, depth) when depth > 0 do {[], rest} end defp decode(<<@bool_false, rest::binary>>, depth) do {values, tail} = decode(rest, depth) {[false | values], tail} end defp decode(<<@bool_true, rest::binary>>, depth) do {values, tail} = decode(rest, depth) {[true | values], tail} end defp decode(<<@binary, rest::binary>>, depth) do {decoded_binary, rest} = decode_binary(rest) {values, tail} = decode(rest, depth) {[decoded_binary | values], tail} end defp decode(<<@nested_list, rest::binary>>, depth) do {nested_values, rest} = decode(rest, depth + 1) {values, tail} = decode(rest, depth) {[nested_values | values], tail} end defp decode(<<@int_neg_8, rest::binary>>, depth), do: dec_neg_int(8, rest, depth) defp decode(<<@int_neg_7, rest::binary>>, depth), do: dec_neg_int(7, rest, depth) defp decode(<<@int_neg_6, rest::binary>>, depth), do: dec_neg_int(6, rest, depth) defp decode(<<@int_neg_5, rest::binary>>, depth), do: dec_neg_int(5, rest, depth) defp decode(<<@int_neg_4, rest::binary>>, depth), do: dec_neg_int(4, rest, depth) defp decode(<<@int_neg_3, rest::binary>>, depth), do: dec_neg_int(3, rest, depth) defp decode(<<@int_neg_2, rest::binary>>, depth), do: dec_neg_int(2, rest, depth) defp decode(<<@int_neg_1, rest::binary>>, depth), do: dec_neg_int(1, rest, depth) defp decode(<<@int_0, rest::binary>>, depth) do {values, tail} = decode(rest, depth) {[0 | values], tail} end defp decode(<<@int_pos_1, rest::binary>>, depth), do: dec_pos_int(1, rest, depth) defp decode(<<@int_pos_2, rest::binary>>, depth), do: dec_pos_int(2, rest, depth) defp decode(<<@int_pos_3, rest::binary>>, depth), do: dec_pos_int(3, rest, depth) defp decode(<<@int_pos_4, rest::binary>>, depth), do: dec_pos_int(4, rest, depth) defp decode(<<@int_pos_5, rest::binary>>, depth), do: dec_pos_int(5, rest, depth) defp decode(<<@int_pos_6, rest::binary>>, depth), do: dec_pos_int(6, rest, depth) defp decode(<<@int_pos_7, rest::binary>>, depth), do: dec_pos_int(7, rest, depth) defp decode(<<@int_pos_8, rest::binary>>, depth), do: dec_pos_int(8, rest, depth) defp decode_binary(binary) do {parts, tail} = decode_binary(binary, 0) {IO.iodata_to_binary(parts), tail} end defp decode_binary(binary, offset) do case binary do <> -> # Un-escape null byte and keep going {parts, tail} = decode_binary(bin_tail, 0) {[head, "\x00" | parts], tail} <> -> # Found null byte without escape, terminate binary (null-terminated) {[head], tail} <<_head::binary-size(offset), _other, _tail::binary>> -> # Found any other byte, keep scanning decode_binary(binary, offset + 1) <<_head::binary-size(offset)>> -> raise "Encoded binary was not terminated: #{inspect(binary)}" end end defp dec_neg_int(bytes, rest, depth) do <> = rest int = int - Bitwise.bsl(1, bytes * 8) + 1 {values, tail} = decode(rest, depth) {[int | values], tail} end defp dec_pos_int(bytes, rest, depth) do <> = rest {values, tail} = decode(rest, depth) {[int | values], tail} end end