Skip to content

Commit 7c10b7d

Browse files
authored
Clean up the API to expose Huffman encoding (#13)
1 parent 4699d88 commit 7c10b7d

File tree

4 files changed

+68
-35
lines changed

4 files changed

+68
-35
lines changed

lib/hpax.ex

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,46 @@ defmodule HPAX do
3131

3232
@valid_header_actions [:store, :store_name, :no_store, :never_store]
3333

34+
@doc """
35+
Creates a new HPACK table.
36+
37+
Same as `new/2` with default options.
38+
"""
39+
@spec new(non_neg_integer()) :: Table.t()
40+
def new(max_table_size), do: new(max_table_size, [])
41+
3442
@doc """
3543
Create a new HPACK table that can be used as encoding or decoding context.
3644
3745
See the "Encoding and decoding contexts" section in the module documentation.
3846
3947
`max_table_size` is the maximum table size (in bytes) for the newly created table.
4048
49+
## Options
50+
51+
This function accepts the following `options`:
52+
53+
* `:huffman_encoding` - (since 0.3.0) `:always` or `:never`. If `:always`,
54+
then HPAX will always encode headers using Huffman encoding. If `:never`,
55+
HPAX will not use any Huffman encoding. Defaults to `:never`.
56+
4157
## Examples
4258
4359
encoding_context = HPAX.new(4096)
4460
4561
"""
46-
@spec new(non_neg_integer(), [Table.option()]) :: Table.t()
47-
def new(max_table_size, options \\ [])
48-
when is_integer(max_table_size) and max_table_size >= 0 do
49-
Table.new(max_table_size, options)
62+
@doc since: "0.3.0"
63+
@spec new(non_neg_integer(), [keyword()]) :: Table.t()
64+
def new(max_table_size, options)
65+
when is_integer(max_table_size) and max_table_size >= 0 and is_list(options) do
66+
options = Keyword.put_new(options, :huffman_encoding, :never)
67+
68+
Enum.each(options, fn
69+
{:huffman_encoding, _huffman_encoding} -> :ok
70+
{key, _value} -> raise ArgumentError, "unknown option: #{inspect(key)}"
71+
end)
72+
73+
Table.new(max_table_size, Keyword.fetch!(options, :huffman_encoding))
5074
end
5175

5276
@doc """
@@ -252,7 +276,7 @@ defmodule HPAX do
252276

253277
defp encode_headers([{action, name, value} | rest], table, acc)
254278
when action in @valid_header_actions and is_binary(name) and is_binary(value) do
255-
huffman? = table.huffman == :always
279+
huffman? = table.huffman_encoding == :always
256280

257281
{encoded, table} =
258282
case Table.lookup_by_header(table, name, value) do
@@ -287,27 +311,27 @@ defmodule HPAX do
287311
<<1::1, Types.encode_integer(index, 7)::bitstring>>
288312
end
289313

290-
defp encode_literal_header_with_indexing(index, value, huffman) when is_integer(index) do
291-
[<<1::2, Types.encode_integer(index, 6)::bitstring>>, Types.encode_binary(value, huffman)]
314+
defp encode_literal_header_with_indexing(index, value, huffman?) when is_integer(index) do
315+
[<<1::2, Types.encode_integer(index, 6)::bitstring>>, Types.encode_binary(value, huffman?)]
292316
end
293317

294-
defp encode_literal_header_with_indexing(name, value, huffman) when is_binary(name) do
295-
[<<1::2, 0::6>>, Types.encode_binary(name, huffman), Types.encode_binary(value, huffman)]
318+
defp encode_literal_header_with_indexing(name, value, huffman?) when is_binary(name) do
319+
[<<1::2, 0::6>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
296320
end
297321

298-
defp encode_literal_header_without_indexing(index, value, huffman) when is_integer(index) do
299-
[<<0::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman)]
322+
defp encode_literal_header_without_indexing(index, value, huffman?) when is_integer(index) do
323+
[<<0::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman?)]
300324
end
301325

302-
defp encode_literal_header_without_indexing(name, value, huffman) when is_binary(name) do
303-
[<<0::4, 0::4>>, Types.encode_binary(name, huffman), Types.encode_binary(value, huffman)]
326+
defp encode_literal_header_without_indexing(name, value, huffman?) when is_binary(name) do
327+
[<<0::4, 0::4>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
304328
end
305329

306-
defp encode_literal_header_never_indexed(index, value, huffman) when is_integer(index) do
307-
[<<1::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman)]
330+
defp encode_literal_header_never_indexed(index, value, huffman?) when is_integer(index) do
331+
[<<1::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman?)]
308332
end
309333

310-
defp encode_literal_header_never_indexed(name, value, huffman) when is_binary(name) do
311-
[<<1::4, 0::4>>, Types.encode_binary(name, huffman), Types.encode_binary(value, huffman)]
334+
defp encode_literal_header_never_indexed(name, value, huffman?) when is_binary(name) do
335+
[<<1::4, 0::4>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
312336
end
313337
end

lib/hpax/table.ex

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
defmodule HPAX.Table do
22
@moduledoc false
33

4+
@enforce_keys [:max_table_size, :huffman_encoding]
45
defstruct [
56
:max_table_size,
6-
:huffman,
7+
:huffman_encoding,
78
entries: [],
89
size: 0,
910
length: 0
1011
]
1112

13+
@type huffman_encoding() :: :always | :never
14+
1215
@type t() :: %__MODULE__{
1316
max_table_size: non_neg_integer(),
14-
huffman: huffman_strategy(),
17+
huffman_encoding: huffman_encoding(),
1518
entries: [{binary(), binary()}],
1619
size: non_neg_integer(),
1720
length: non_neg_integer()
1821
}
1922

20-
@type huffman_strategy() :: :always | :never
21-
@type option :: {:huffman, huffman_strategy()}
22-
2323
@static_table [
2424
{":authority", nil},
2525
{":method", "GET"},
@@ -93,10 +93,11 @@ defmodule HPAX.Table do
9393
The maximum size is not the maximum number of entries but rather the maximum size as defined in
9494
http://httpwg.org/specs/rfc7541.html#maximum.table.size.
9595
"""
96-
@spec new(non_neg_integer(), [option()]) :: t()
97-
def new(max_table_size, options \\ []) do
98-
huffman = Keyword.get(options, :huffman, :always)
99-
%__MODULE__{max_table_size: max_table_size, huffman: huffman}
96+
@spec new(non_neg_integer(), huffman_encoding()) :: t()
97+
def new(max_table_size, huffman_encoding)
98+
when is_integer(max_table_size) and max_table_size >= 0 and
99+
huffman_encoding in [:always, :never] do
100+
%__MODULE__{max_table_size: max_table_size, huffman_encoding: huffman_encoding}
100101
end
101102

102103
@doc """

test/hpax/table_test.exs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ defmodule HPAX.TableTest do
55
alias HPAX.Table
66

77
test "new/1" do
8-
assert %Table{} = Table.new(100)
8+
assert %Table{} = Table.new(100, :always)
99
end
1010

1111
test "adding headers and fetching them by value" do
12-
table = Table.new(10_000)
12+
table = Table.new(10_000, :always)
1313

1414
# These are in the static table.
1515
assert {:full, _} = Table.lookup_by_header(table, ":status", "200")
@@ -30,7 +30,7 @@ defmodule HPAX.TableTest do
3030
dynamic_table_start = length(Table.__static_table__()) + 1
3131

3232
# This fits two headers that have name and value of 4 bytes (4 + 4 + 32, twice).
33-
table = Table.new(80)
33+
table = Table.new(80, :always)
3434

3535
table = Table.add(table, "aaaa", "AAAA")
3636
table = Table.add(table, "bbbb", "BBBB")
@@ -50,15 +50,15 @@ defmodule HPAX.TableTest do
5050

5151
describe "looking headers up by index" do
5252
test "with an index out of bounds" do
53-
assert Table.lookup_by_index(Table.new(100), 1000) == :error
53+
assert Table.lookup_by_index(Table.new(100, :never), 1000) == :error
5454
end
5555

5656
test "with an index in the static table" do
57-
assert Table.lookup_by_index(Table.new(100), 1) == {:ok, {":authority", nil}}
57+
assert Table.lookup_by_index(Table.new(100, :never), 1) == {:ok, {":authority", nil}}
5858
end
5959

6060
test "with an index in the dynamic table" do
61-
table = Table.new(100)
61+
table = Table.new(100, :never)
6262
table = Table.add(table, "my-header", "my-value")
6363

6464
assert Table.lookup_by_index(table, length(Table.__static_table__()) + 1) ==
@@ -68,7 +68,7 @@ defmodule HPAX.TableTest do
6868

6969
property "adding a header and then looking it up always returns the index of that header" do
7070
check all {name, value} <- {string(0..127, min_length: 1), binary()} do
71-
assert %Table{} = table = Table.new(10_000)
71+
assert %Table{} = table = Table.new(10_000, :never)
7272
assert %Table{} = table = Table.add(table, name, value)
7373
assert {:full, 62} = Table.lookup_by_header(table, name, value)
7474
end

test/hpax_test.exs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ defmodule HPAXTest do
66
assert %HPAX.Table{} = HPAX.new(100)
77
end
88

9+
describe "new/2" do
10+
test "raises for unknown options" do
11+
assert_raise ArgumentError, "unknown option: :unknown", fn ->
12+
HPAX.new(100, unknown: :option)
13+
end
14+
end
15+
end
16+
917
# https://http2.github.io/http2-spec/compression.html#rfc.section.C.2.1
1018
test "decode/2 with an example from the spec" do
1119
table = HPAX.new(1000)
@@ -23,7 +31,7 @@ defmodule HPAXTest do
2331

2432
# https://http2.github.io/http2-spec/compression.html#rfc.section.C.3.1
2533
test "encode/2 with a literal example from the spec" do
26-
table = HPAX.new(1000, huffman: :never)
34+
table = HPAX.new(1000, huffman_encoding: :never)
2735

2836
headers = [
2937
{:store, ":method", "GET"},
@@ -43,7 +51,7 @@ defmodule HPAXTest do
4351

4452
# https://http2.github.io/http2-spec/compression.html#rfc.section.C.4.1
4553
test "encode/2 with a Huffman example from the spec" do
46-
table = HPAX.new(1000)
54+
table = HPAX.new(1000, huffman_encoding: :always)
4755

4856
headers = [
4957
{:store, ":method", "GET"},

0 commit comments

Comments
 (0)