Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 51 additions & 51 deletions lib/hpax/table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ defmodule HPAX.Table do
}

@static_table [
{":authority", nil},
{":authority", ""},
{":method", "GET"},
{":method", "POST"},
{":path", "/"},
Expand All @@ -39,53 +39,53 @@ defmodule HPAX.Table do
{":status", "400"},
{":status", "404"},
{":status", "500"},
{"accept-charset", nil},
{"accept-charset", ""},
{"accept-encoding", "gzip, deflate"},
{"accept-language", nil},
{"accept-ranges", nil},
{"accept", nil},
{"access-control-allow-origin", nil},
{"age", nil},
{"allow", nil},
{"authorization", nil},
{"cache-control", nil},
{"content-disposition", nil},
{"content-encoding", nil},
{"content-language", nil},
{"content-length", nil},
{"content-location", nil},
{"content-range", nil},
{"content-type", nil},
{"cookie", nil},
{"date", nil},
{"etag", nil},
{"expect", nil},
{"expires", nil},
{"from", nil},
{"host", nil},
{"if-match", nil},
{"if-modified-since", nil},
{"if-none-match", nil},
{"if-range", nil},
{"if-unmodified-since", nil},
{"last-modified", nil},
{"link", nil},
{"location", nil},
{"max-forwards", nil},
{"proxy-authenticate", nil},
{"proxy-authorization", nil},
{"range", nil},
{"referer", nil},
{"refresh", nil},
{"retry-after", nil},
{"server", nil},
{"set-cookie", nil},
{"strict-transport-security", nil},
{"transfer-encoding", nil},
{"user-agent", nil},
{"vary", nil},
{"via", nil},
{"www-authenticate", nil}
{"accept-language", ""},
{"accept-ranges", ""},
{"accept", ""},
{"access-control-allow-origin", ""},
{"age", ""},
{"allow", ""},
{"authorization", ""},
{"cache-control", ""},
{"content-disposition", ""},
{"content-encoding", ""},
{"content-language", ""},
{"content-length", ""},
{"content-location", ""},
{"content-range", ""},
{"content-type", ""},
{"cookie", ""},
{"date", ""},
{"etag", ""},
{"expect", ""},
{"expires", ""},
{"from", ""},
{"host", ""},
{"if-match", ""},
{"if-modified-since", ""},
{"if-none-match", ""},
{"if-range", ""},
{"if-unmodified-since", ""},
{"last-modified", ""},
{"link", ""},
{"location", ""},
{"max-forwards", ""},
{"proxy-authenticate", ""},
{"proxy-authorization", ""},
{"range", ""},
{"referer", ""},
{"refresh", ""},
{"retry-after", ""},
{"server", ""},
{"set-cookie", ""},
{"strict-transport-security", ""},
{"transfer-encoding", ""},
{"user-agent", ""},
{"vary", ""},
{"via", ""},
{"www-authenticate", ""}
]

@static_table_size length(@static_table)
Expand Down Expand Up @@ -149,10 +149,10 @@ defmodule HPAX.Table do
Looks up a header by index `index` in the given `table`.

Returns `{:ok, {name, value}}` if a header is found at the given `index`, otherwise returns
`:error`. `value` can be a binary in case both the header name and value are present in the
table, or `nil` if only the name is present (this can only happen in the static table).
`:error`. Some static table entries (see RFC 7541, Appendix A) have no defined value; `value`
is the empty binary `""` for those.
"""
@spec lookup_by_index(t(), pos_integer()) :: {:ok, {binary(), binary() | nil}} | :error
@spec lookup_by_index(t(), pos_integer()) :: {:ok, {binary(), binary()}} | :error
def lookup_by_index(table, index)

# Static table
Expand Down Expand Up @@ -190,7 +190,7 @@ defmodule HPAX.Table do
> header field names MUST be converted to lowercase prior to their encoding in HTTP/2

"""
@spec lookup_by_header(t(), binary(), binary() | nil) ::
@spec lookup_by_header(t(), binary(), binary()) ::
{:full, pos_integer()} | {:name, pos_integer()} | :not_found
def lookup_by_header(table, name, value)

Expand Down
11 changes: 7 additions & 4 deletions test/hpax/table_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ defmodule HPAX.TableTest do

# These are in the static table.
assert {:full, _} = Table.lookup_by_header(table, ":status", "200")
assert {:name, _} = Table.lookup_by_header(table, ":authority", nil)
# :authority has no defined value in the static table (RFC 7541, Appendix A), which is
# represented as the empty binary, so it's a full match against "" and a name-only match
# against anything else.
assert {:full, _} = Table.lookup_by_header(table, ":authority", "")
assert {:name, _} = Table.lookup_by_header(table, ":authority", "https://example.com")

assert Table.lookup_by_header(table, "my-nonexistent-header", nil) == :not_found
assert Table.lookup_by_header(table, "my-nonexistent-header", "") == :not_found
assert Table.lookup_by_header(table, "my-nonexistent-header", "my-value") == :not_found

table = Table.add(table, ":my-header", "my-value")

assert {:full, _} = Table.lookup_by_header(table, ":my-header", "my-value")
assert {:name, _} = Table.lookup_by_header(table, ":my-header", "other-value")
assert {:name, _} = Table.lookup_by_header(table, ":my-header", nil)
assert {:name, _} = Table.lookup_by_header(table, ":my-header", "")
end

test "LRU eviction" do
Expand All @@ -50,7 +53,7 @@ defmodule HPAX.TableTest do
end

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

test "with an index in the dynamic table" do
Expand Down
24 changes: 23 additions & 1 deletion test/hpax_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ defmodule HPAXTest do
assert headers == [{"custom-key", "custom-header"}]
end

test "decode/2 of a fully-indexed reference to a static entry with no defined value returns an empty binary, not nil" do
table = HPAX.new(1000)

# Index 58 is "user-agent" (RFC 7541, Appendix A), which has no defined default value.
# A peer is allowed to reference it via a plain Indexed Header Field Representation
# (section 6.1); the decoded value must be a binary per this module's documented contract,
# not the atom `nil`.
assert {:ok, [{"user-agent", value}], %HPAX.Table{}} = HPAX.decode(<<0xBA>>, table)
assert value == ""
end

# https://http2.github.io/http2-spec/compression.html#rfc.section.C.3.1
test "encode/2 with a literal example from the spec" do
table = HPAX.new(1000, huffman_encoding: :never)
Expand All @@ -49,6 +60,17 @@ defmodule HPAXTest do
assert IO.iodata_to_binary(encoded) == expected
end

test "encode/2 emits a compact indexed reference for a header whose value matches a static entry with no defined default value" do
table = HPAX.new(1000, huffman_encoding: :never)

# Index 58 is "user-agent" (RFC 7541, Appendix A), which has no defined default value in
# the static table. Encoding it with a literal "" value should still find a full match
# there and produce a single indexed-header-field byte (section 6.1), rather than falling
# back to a longer literal representation.
assert {encoded, %HPAX.Table{}} = HPAX.encode([{:store, "user-agent", ""}], table)
assert IO.iodata_to_binary(encoded) == <<0xBA>>
end

# https://http2.github.io/http2-spec/compression.html#rfc.section.C.4.1
test "encode/2 with a Huffman example from the spec" do
table = HPAX.new(1000, huffman_encoding: :always)
Expand Down Expand Up @@ -289,7 +311,7 @@ defmodule HPAXTest do
defp header() do
header_from_static_table =
bind(member_of(HPAX.Table.__static_table__()), fn
{name, nil} -> {constant(name), binary()}
{name, ""} -> {constant(name), binary()}
{name, value} -> constant({name, value})
end)

Expand Down
Loading