this repo has no description
1defmodule Hobbes.Encoding.Keyset do
2 @moduledoc """
3 Encodings for Hobbes keys and values.
4 """
5
6 @escape 0xFF
7 @nil_value 0x00
8 @binary 0x01
9 @nested_list 0x05
10
11 @int_neg_8 0x0C
12 @int_neg_7 0x0D
13 @int_neg_6 0x0E
14 @int_neg_5 0x0F
15 @int_neg_4 0x10
16 @int_neg_3 0x11
17 @int_neg_2 0x12
18 @int_neg_1 0x13
19 @int_0 0x14
20 @int_pos_1 0x15
21 @int_pos_2 0x16
22 @int_pos_3 0x17
23 @int_pos_4 0x18
24 @int_pos_5 0x1A
25 @int_pos_6 0x1B
26 @int_pos_7 0x1C
27 @int_pos_8 0x1D
28
29 @bool_false 0x26
30 @bool_true 0x27
31
32 @spec pack(list) :: binary
33 def pack(list) when is_list(list) do
34 Enum.map(list, &encode(&1, 0))
35 |> IO.iodata_to_binary()
36 end
37
38 def pack(term) do
39 raise """
40 Keyset.pack/1 expects a top-level list, got: #{inspect(term)}
41 If you want to encode a single term, wrap it in a list, like: Keyset.pack([123])
42 """
43 end
44
45 @spec unpack(binary) :: list
46 def unpack(binary) when is_binary(binary) do
47 case decode(binary, 0) do
48 {values, ""} ->
49 values
50 end
51 end
52
53 defp encode(nil, 0), do: <<@nil_value>>
54 defp encode(nil, depth) when depth > 0, do: <<@nil_value, @escape>>
55
56 defp encode(false, _depth), do: <<@bool_false>>
57 defp encode(true, _depth), do: <<@bool_true>>
58
59 defp encode(list, depth) when is_list(list) do
60 values = Enum.map(list, &encode(&1, depth + 1))
61 [@nested_list, values, @nil_value]
62 end
63
64 defp encode(binary, _depth) when is_binary(binary) do
65 [<<@binary>>, encode_binary(binary, 0)]
66 end
67
68 defp encode(int, _depth) when is_integer(int) and int < 0 do
69 binary = :binary.encode_unsigned(-int)
70 binary = for <<b <- binary>>, into: "", do: <<Bitwise.bxor(b, 0xFF)>>
71 case byte_size(binary) do
72 8 -> [<<@int_neg_8>>, binary]
73 7 -> [<<@int_neg_7>>, binary]
74 6 -> [<<@int_neg_6>>, binary]
75 5 -> [<<@int_neg_5>>, binary]
76 4 -> [<<@int_neg_4>>, binary]
77 3 -> [<<@int_neg_3>>, binary]
78 2 -> [<<@int_neg_2>>, binary]
79 1 -> [<<@int_neg_1>>, binary]
80 end
81 end
82
83 defp encode(0, _depth) do
84 <<@int_0>>
85 end
86
87 defp encode(int, _depth) when is_integer(int) and int > 0 do
88 binary = :binary.encode_unsigned(int)
89 case byte_size(binary) do
90 1 -> [<<@int_pos_1>>, binary]
91 2 -> [<<@int_pos_2>>, binary]
92 3 -> [<<@int_pos_3>>, binary]
93 4 -> [<<@int_pos_4>>, binary]
94 5 -> [<<@int_pos_5>>, binary]
95 6 -> [<<@int_pos_6>>, binary]
96 7 -> [<<@int_pos_7>>, binary]
97 8 -> [<<@int_pos_8>>, binary]
98 end
99 end
100
101 # Encodes a null-terminated binary, nulls are escaped as <<0x00, 0xFF>>
102 defp encode_binary(binary, offset) do
103 case binary do
104 <<head::binary-size(offset)>> ->
105 # Terminate encoded binary (null-terminated)
106 [head, <<@nil_value>>]
107
108 <<head::binary-size(offset), 0x00, tail::binary>> ->
109 # Escape null value in binary and keep going
110 [head, <<@nil_value, @escape>> | encode_binary(tail, 0)]
111
112 <<_head::binary-size(offset), _other, _tail::binary>> ->
113 # Non-null character, keep scanning
114 encode_binary(binary, offset + 1)
115 end
116 end
117
118 defp decode("", 0) do
119 {[], ""}
120 end
121
122 defp decode(<<@nil_value, rest::binary>>, 0 = depth) do
123 {values, tail} = decode(rest, depth)
124 {[nil | values], tail}
125 end
126
127 # Escaped nil within a nested list
128 defp decode(<<@nil_value, @escape, rest::binary>>, depth) when depth > 0 do
129 {values, tail} = decode(rest, depth)
130 {[nil | values], tail}
131 end
132
133 # Closing a nested list
134 defp decode(<<@nil_value, rest::binary>>, depth) when depth > 0 do
135 {[], rest}
136 end
137
138 defp decode(<<@bool_false, rest::binary>>, depth) do
139 {values, tail} = decode(rest, depth)
140 {[false | values], tail}
141 end
142
143 defp decode(<<@bool_true, rest::binary>>, depth) do
144 {values, tail} = decode(rest, depth)
145 {[true | values], tail}
146 end
147
148 defp decode(<<@binary, rest::binary>>, depth) do
149 {decoded_binary, rest} = decode_binary(rest)
150 {values, tail} = decode(rest, depth)
151 {[decoded_binary | values], tail}
152 end
153
154 defp decode(<<@nested_list, rest::binary>>, depth) do
155 {nested_values, rest} = decode(rest, depth + 1)
156 {values, tail} = decode(rest, depth)
157 {[nested_values | values], tail}
158 end
159
160 defp decode(<<@int_neg_8, rest::binary>>, depth), do: dec_neg_int(8, rest, depth)
161 defp decode(<<@int_neg_7, rest::binary>>, depth), do: dec_neg_int(7, rest, depth)
162 defp decode(<<@int_neg_6, rest::binary>>, depth), do: dec_neg_int(6, rest, depth)
163 defp decode(<<@int_neg_5, rest::binary>>, depth), do: dec_neg_int(5, rest, depth)
164 defp decode(<<@int_neg_4, rest::binary>>, depth), do: dec_neg_int(4, rest, depth)
165 defp decode(<<@int_neg_3, rest::binary>>, depth), do: dec_neg_int(3, rest, depth)
166 defp decode(<<@int_neg_2, rest::binary>>, depth), do: dec_neg_int(2, rest, depth)
167 defp decode(<<@int_neg_1, rest::binary>>, depth), do: dec_neg_int(1, rest, depth)
168
169 defp decode(<<@int_0, rest::binary>>, depth) do
170 {values, tail} = decode(rest, depth)
171 {[0 | values], tail}
172 end
173
174 defp decode(<<@int_pos_1, rest::binary>>, depth), do: dec_pos_int(1, rest, depth)
175 defp decode(<<@int_pos_2, rest::binary>>, depth), do: dec_pos_int(2, rest, depth)
176 defp decode(<<@int_pos_3, rest::binary>>, depth), do: dec_pos_int(3, rest, depth)
177 defp decode(<<@int_pos_4, rest::binary>>, depth), do: dec_pos_int(4, rest, depth)
178 defp decode(<<@int_pos_5, rest::binary>>, depth), do: dec_pos_int(5, rest, depth)
179 defp decode(<<@int_pos_6, rest::binary>>, depth), do: dec_pos_int(6, rest, depth)
180 defp decode(<<@int_pos_7, rest::binary>>, depth), do: dec_pos_int(7, rest, depth)
181 defp decode(<<@int_pos_8, rest::binary>>, depth), do: dec_pos_int(8, rest, depth)
182
183 defp decode_binary(binary) do
184 {parts, tail} = decode_binary(binary, 0)
185 {IO.iodata_to_binary(parts), tail}
186 end
187
188 defp decode_binary(binary, offset) do
189 case binary do
190 <<head::binary-size(offset), @nil_value, @escape, bin_tail::binary>> ->
191 # Un-escape null byte and keep going
192 {parts, tail} = decode_binary(bin_tail, 0)
193 {[head, "\x00" | parts], tail}
194
195 <<head::binary-size(offset), @nil_value, tail::binary>> ->
196 # Found null byte without escape, terminate binary (null-terminated)
197 {[head], tail}
198
199 <<_head::binary-size(offset), _other, _tail::binary>> ->
200 # Found any other byte, keep scanning
201 decode_binary(binary, offset + 1)
202
203 <<_head::binary-size(offset)>> -> raise "Encoded binary was not terminated: #{inspect(binary)}"
204 end
205 end
206
207 defp dec_neg_int(bytes, rest, depth) do
208 <<int::unsigned-big-integer-unit(8)-size(bytes), rest::binary>> = rest
209 int = int - Bitwise.bsl(1, bytes * 8) + 1
210
211 {values, tail} = decode(rest, depth)
212 {[int | values], tail}
213 end
214
215 defp dec_pos_int(bytes, rest, depth) do
216 <<int::unsigned-big-integer-size(bytes)-unit(8), rest::binary>> = rest
217
218 {values, tail} = decode(rest, depth)
219 {[int | values], tail}
220 end
221end