This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:stream_data]
]

BIN
phoenix/deps/hpax/.hex Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
# Changelog
## v1.0.3
* Silence warnings on (upcoming, at this time) Elixir 1.19+.
## v1.0.2
* The changes in v1.0.1 introduced some subtle compression errors with HPACK encoding. This has been fixed in this version. See [this issue](https://github.com/elixir-mint/hpax/issues/20) for more details.
## v1.0.1
* Fix some issues with dynamic table resizing. You should not need to do anything to your code, it should Just Work™. If you want to read more, [this issue](https://github.com/elixir-mint/hpax/issues/18) has all the context.
## v1.0.0
* Silence warnings on Elixir 1.17+.
* Require Elixir 1.12+.
## v0.2.0
* Add `HPAX.new/2`, which supports a list of options. For now, the only option
is `:huffman_encoding`, to choose whether to use Huffman encoding or not.
* Add `HPAX.encode/3`, which supports encoding all headers with the same
action.
* Add the `HPAX.table/0` opaque type.
## v0.1.2
* Fix `use Bitwise` deprecation warning.
## v0.1.1
* Improve checking of dynamic resize updates.

View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,74 @@
# HPAX
![CI](https://github.com/elixir-mint/hpax/actions/workflows/main.yml/badge.svg)
[![Docs](https://img.shields.io/badge/api-docs-green.svg?style=flat)](https://hexdocs.pm/hpax)
[![Hex.pm Version](http://img.shields.io/hexpm/v/hpax.svg?style=flat)](https://hex.pm/packages/hpax)
[![Coverage Status](https://coveralls.io/repos/github/elixir-mint/hpax/badge.svg?branch=main)](https://coveralls.io/github/elixir-mint/hpax?branch=main)
HPAX is an Elixir implementation of the HPACK header compression algorithm as used in HTTP/2 and
defined in RFC 7541. HPAX is used by several Elixir projects, including the
[Mint](https://github.com/elixir-mint/mint) HTTP client and
[bandit](https://github.com/mtrudel/bandit) HTTP server projects.
## Installation
To install HPAX, add it to your `mix.exs` file.
```elixir
defp deps do
[
{:hpax, "~> 0.1.0"}
]
end
```
Then, run `$ mix deps.get`.
## Usage
HPAX is designed to be used in both encoding and decoding scenarios. In both cases, a context is
used to maintain state internal to the HPACK algorithm. In the common use case of using HPAX
within HTTP/2, this context is called a **table** and must be shared between any
subsequent encoding/decoding calls within
an endpoint. Note that the contexts used for encoding and decoding within HTTP/2 are completely
distinct from one another, even though they are structurally identical.
To encode a set of headers into a binary with HPAX:
```elixir
context = HPAX.new(4096)
headers = [{:store, ":status", "201"}, {:store, "location", "http://example.com"}]
{encoded_headers, context} = HPAX.encode(headers, context)
#=> {iodata, updated_context}
```
To decode a binary into a set of headers with HPAX:
```elixir
context = HPAX.new(4096)
encoded_headers = <<...>>
{:ok, headers, context} = HPAX.decode(encoded_headers, context)
#=> {:ok, [{:store, ":status", "201"}, {:store, "location", "http://example.com"}], updated_context}
```
For complete usage information, please see the HPAX [documentation](https://hex.pm/packages/hpax).
## Contributing
If you wish to contribute check out the [issue list](https://github.com/elixir-mint/hpax/issues) and let us know what you want to work on so we can discuss it and reduce duplicate work.
## License
Copyright 2021 Eric Meadows-Jönsson and Andrea Leopardi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,15 @@
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/elixir-mint/hpax">>}]}.
{<<"name">>,<<"hpax">>}.
{<<"version">>,<<"1.0.3">>}.
{<<"description">>,
<<"Implementation of the HPACK protocol (RFC 7541) for Elixir">>}.
{<<"elixir">>,<<"~> 1.12">>}.
{<<"app">>,<<"hpax">>}.
{<<"licenses">>,[<<"Apache-2.0">>]}.
{<<"requirements">>,[]}.
{<<"files">>,
[<<"lib">>,<<"lib/hpax">>,<<"lib/hpax/huffman_table">>,
<<"lib/hpax/types.ex">>,<<"lib/hpax/huffman.ex">>,<<"lib/hpax/table.ex">>,
<<"lib/hpax.ex">>,<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,
<<"LICENSE.txt">>,<<"CHANGELOG.md">>]}.
{<<"build_tools">>,[<<"mix">>]}.

View File

@@ -0,0 +1,360 @@
defmodule HPAX do
@moduledoc """
Support for the HPACK header compression algorithm.
This module provides support for the HPACK header compression algorithm used mainly in HTTP/2.
## Encoding and decoding contexts
The HPACK algorithm requires both
* an encoding context on the encoder side
* a decoding context on the decoder side
These contexts are semantically different but structurally the same. In HPACK they are
implemented as **HPACK tables**. This library uses the name "tables" everywhere internally
HPACK tables can be created through the `new/1` function.
"""
alias HPAX.{Table, Types}
@typedoc """
An HPACK table.
This can be used for encoding or decoding.
"""
@typedoc since: "0.2.0"
@opaque table() :: Table.t()
@typedoc """
An HPACK header name.
"""
@type header_name() :: binary()
@typedoc """
An HPACK header value.
"""
@type header_value() :: binary()
@valid_header_actions [:store, :store_name, :no_store, :never_store]
@doc """
Creates a new HPACK table.
Same as `new/2` with default options.
"""
@spec new(non_neg_integer()) :: table()
def new(max_table_size), do: new(max_table_size, [])
@doc """
Create a new HPACK table that can be used as encoding or decoding context.
See the "Encoding and decoding contexts" section in the module documentation.
`max_table_size` is the maximum table size (in bytes) for the newly created table.
## Options
This function accepts the following `options`:
* `:huffman_encoding` - (since 0.2.0) `:always` or `:never`. If `:always`,
then HPAX will always encode headers using Huffman encoding. If `:never`,
HPAX will not use any Huffman encoding. Defaults to `:never`.
## Examples
encoding_context = HPAX.new(4096)
"""
@doc since: "0.2.0"
@spec new(non_neg_integer(), [keyword()]) :: table()
def new(max_table_size, options)
when is_integer(max_table_size) and max_table_size >= 0 and is_list(options) do
options = Keyword.put_new(options, :huffman_encoding, :never)
Enum.each(options, fn
{:huffman_encoding, _huffman_encoding} -> :ok
{key, _value} -> raise ArgumentError, "unknown option: #{inspect(key)}"
end)
Table.new(max_table_size, Keyword.fetch!(options, :huffman_encoding))
end
@doc """
Resizes the given table to the given maximum size.
This is intended for use where the overlying protocol has signaled a change to the table's
maximum size, such as when an HTTP/2 `SETTINGS` frame is received.
If the indicated size is less than the table's current size, entries
will be evicted as needed to fit within the specified size, and the table's
maximum size will be decreased to the specified value. A flag will also be
set which will enqueue a "dynamic table size update" command to be prefixed
to the next block encoded with this table, per
[RFC9113§4.3.1](https://www.rfc-editor.org/rfc/rfc9113.html#section-4.3.1).
If the indicated size is greater than or equal to the table's current max size, no entries are evicted
and the table's maximum size changes to the specified value.
## Examples
decoding_context = HPAX.new(4096)
HPAX.resize(decoding_context, 8192)
"""
@spec resize(table(), non_neg_integer()) :: table()
defdelegate resize(table, new_max_size), to: Table
@doc """
Decodes a header block fragment (HBF) through a given table.
If decoding is successful, this function returns a `{:ok, headers, updated_table}` tuple where
`headers` is a list of decoded headers, and `updated_table` is the updated table. If there's
an error in decoding, this function returns `{:error, reason}`.
## Examples
decoding_context = HPAX.new(1000)
hbf = get_hbf_from_somewhere()
HPAX.decode(hbf, decoding_context)
#=> {:ok, [{":method", "GET"}], decoding_context}
"""
@spec decode(binary(), table()) ::
{:ok, [{header_name(), header_value()}], table()} | {:error, term()}
# Dynamic resizes must occur only at the start of a block
# https://datatracker.ietf.org/doc/html/rfc7541#section-4.2
def decode(<<0b001::3, rest::bitstring>>, %Table{} = table) do
{new_max_size, rest} = decode_integer(rest, 5)
# Dynamic resizes must be less than protocol max table size
# https://datatracker.ietf.org/doc/html/rfc7541#section-6.3
if new_max_size <= table.protocol_max_table_size do
decode(rest, Table.dynamic_resize(table, new_max_size))
else
{:error, :protocol_error}
end
end
def decode(block, %Table{} = table) when is_binary(block) do
decode_headers(block, table, _acc = [])
catch
:throw, {:hpax, error} -> {:error, error}
end
@doc """
Encodes a list of headers through the given table.
Returns a two-element tuple where the first element is a binary representing the encoded headers
and the second element is an updated table.
## Examples
headers = [{:store, ":authority", "https://example.com"}]
encoding_context = HPAX.new(1000)
HPAX.encode(headers, encoding_context)
#=> {iodata, updated_encoding_context}
"""
@spec encode([header], table()) :: {iodata(), table()}
when header: {action, header_name(), header_value()},
action: :store | :store_name | :no_store | :never_store
def encode(headers, %Table{} = table) when is_list(headers) do
{table, pending_resizes} = Table.pop_pending_resizes(table)
acc = Enum.map(pending_resizes, &[<<0b001::3, Types.encode_integer(&1, 5)::bitstring>>])
encode_headers(headers, table, acc)
end
@doc """
Encodes a list of headers through the given table, applying the same `action` to all of them.
This function is the similar to `encode/2`, but `headers` are `{name, value}` tuples instead,
and the same `action` is applied to all headers.
## Examples
headers = [{":authority", "https://example.com"}]
encoding_context = HPAX.new(1000)
HPAX.encode(:store, headers, encoding_context)
#=> {iodata, updated_encoding_context}
"""
@doc since: "0.2.0"
@spec encode(action, [header], table()) :: {iodata(), table()}
when action: :store | :store_name | :no_store | :never_store,
header: {header_name(), header_value()}
def encode(action, headers, %Table{} = table)
when is_list(headers) and action in [:store, :store_name, :no_store, :never_store] do
headers
|> Enum.map(fn {name, value} -> {action, name, value} end)
|> encode(table)
end
## Helpers
defp decode_headers(<<>>, table, acc) do
{:ok, Enum.reverse(acc), table}
end
# Indexed header field
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.1
defp decode_headers(<<0b1::1, rest::bitstring>>, table, acc) do
{index, rest} = decode_integer(rest, 7)
decode_headers(rest, table, [lookup_by_index!(table, index) | acc])
end
# Literal header field with incremental indexing
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
defp decode_headers(<<0b01::2, rest::bitstring>>, table, acc) do
{name, value, rest} =
case rest do
# The header name is a string.
<<0::6, rest::binary>> ->
{name, rest} = decode_binary(rest)
{value, rest} = decode_binary(rest)
{name, value, rest}
# The header name is an index to be looked up in the table.
_other ->
{index, rest} = decode_integer(rest, 6)
{value, rest} = decode_binary(rest)
{name, _value} = lookup_by_index!(table, index)
{name, value, rest}
end
decode_headers(rest, Table.add(table, name, value), [{name, value} | acc])
end
# Literal header field without indexing
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
defp decode_headers(<<0b0000::4, rest::bitstring>>, table, acc) do
{name, value, rest} =
case rest do
<<0::4, rest::binary>> ->
{name, rest} = decode_binary(rest)
{value, rest} = decode_binary(rest)
{name, value, rest}
_other ->
{index, rest} = decode_integer(rest, 4)
{value, rest} = decode_binary(rest)
{name, _value} = lookup_by_index!(table, index)
{name, value, rest}
end
decode_headers(rest, table, [{name, value} | acc])
end
# Literal header field never indexed
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
defp decode_headers(<<0b0001::4, rest::bitstring>>, table, acc) do
{name, value, rest} =
case rest do
<<0::4, rest::binary>> ->
{name, rest} = decode_binary(rest)
{value, rest} = decode_binary(rest)
{name, value, rest}
_other ->
{index, rest} = decode_integer(rest, 4)
{value, rest} = decode_binary(rest)
{name, _value} = lookup_by_index!(table, index)
{name, value, rest}
end
# TODO: enforce the "never indexed" part somehow.
decode_headers(rest, table, [{name, value} | acc])
end
defp decode_headers(_other, _table, _acc) do
throw({:hpax, :protocol_error})
end
defp lookup_by_index!(table, index) do
case Table.lookup_by_index(table, index) do
{:ok, header} -> header
:error -> throw({:hpax, {:index_not_found, index}})
end
end
defp decode_integer(bitstring, prefix) do
case Types.decode_integer(bitstring, prefix) do
{:ok, int, rest} -> {int, rest}
:error -> throw({:hpax, :bad_integer_encoding})
end
end
defp decode_binary(binary) do
case Types.decode_binary(binary) do
{:ok, binary, rest} -> {binary, rest}
:error -> throw({:hpax, :bad_binary_encoding})
end
end
defp encode_headers([], table, acc) do
{acc, table}
end
defp encode_headers([{action, name, value} | rest], table, acc)
when action in @valid_header_actions and is_binary(name) and is_binary(value) do
huffman? = table.huffman_encoding == :always
{encoded, table} =
case Table.lookup_by_header(table, name, value) do
{:full, index} ->
{encode_indexed_header(index), table}
{:name, index} when action == :store ->
{encode_literal_header_with_indexing(index, value, huffman?),
Table.add(table, name, value)}
{:name, index} when action in [:store_name, :no_store] ->
{encode_literal_header_without_indexing(index, value, huffman?), table}
{:name, index} when action == :never_store ->
{encode_literal_header_never_indexed(index, value, huffman?), table}
:not_found when action in [:store, :store_name] ->
{encode_literal_header_with_indexing(name, value, huffman?),
Table.add(table, name, value)}
:not_found when action == :no_store ->
{encode_literal_header_without_indexing(name, value, huffman?), table}
:not_found when action == :never_store ->
{encode_literal_header_never_indexed(name, value, huffman?), table}
end
encode_headers(rest, table, [acc, encoded])
end
defp encode_indexed_header(index) do
<<1::1, Types.encode_integer(index, 7)::bitstring>>
end
defp encode_literal_header_with_indexing(index, value, huffman?) when is_integer(index) do
[<<1::2, Types.encode_integer(index, 6)::bitstring>>, Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_with_indexing(name, value, huffman?) when is_binary(name) do
[<<1::2, 0::6>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_without_indexing(index, value, huffman?) when is_integer(index) do
[<<0::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_without_indexing(name, value, huffman?) when is_binary(name) do
[<<0::4, 0::4>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_never_indexed(index, value, huffman?) when is_integer(index) do
[<<1::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_never_indexed(name, value, huffman?) when is_binary(name) do
[<<1::4, 0::4>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
end
end

View File

@@ -0,0 +1,94 @@
defmodule HPAX.Huffman do
@moduledoc false
import Bitwise, only: [>>>: 2]
# This file is downloaded from the spec directly.
# http://httpwg.org/specs/rfc7541.html#huffman.code
table_file = Path.absname("huffman_table", __DIR__)
@external_resource table_file
entries =
Enum.map(File.stream!(table_file), fn line ->
[byte_value, bits, _hex, bit_count] =
line
|> case do
<<?', _, ?', ?\s, rest::binary>> -> rest
"EOS " <> rest -> rest
_other -> line
end
|> String.replace(["|", "(", ")", "[", "]"], "")
|> String.split()
byte_value = String.to_integer(byte_value)
bits = String.to_integer(bits, 2)
bit_count = String.to_integer(bit_count)
{byte_value, bits, bit_count}
end)
{regular_entries, [eos_entry]} = Enum.split(entries, -1)
{_eos_byte_value, eos_bits, eos_bit_count} = eos_entry
## Encoding
@spec encode(binary()) :: binary()
def encode(binary) do
encode(binary, _acc = <<>>)
end
for {byte_value, bits, bit_count} <- regular_entries do
defp encode(<<unquote(byte_value), rest::binary>>, acc) do
encode(rest, <<acc::bitstring, unquote(bits)::size(unquote(bit_count))>>)
end
end
defp encode(<<>>, acc) do
overflowing_bits = rem(bit_size(acc), 8)
if overflowing_bits == 0 do
acc
else
bits_to_add = 8 - overflowing_bits
value_of_bits_to_add =
take_significant_bits(unquote(eos_bits), unquote(eos_bit_count), bits_to_add)
<<acc::bitstring, value_of_bits_to_add::size(bits_to_add)>>
end
end
## Decoding
@spec decode(binary()) :: binary()
def decode(binary)
for {byte_value, bits, bit_count} <- regular_entries do
def decode(<<unquote(bits)::size(unquote(bit_count)), rest::bitstring>>) do
<<unquote(byte_value), decode(rest)::binary>>
end
end
def decode(<<>>) do
<<>>
end
# Use binary syntax for single match context optimization.
def decode(<<padding::bitstring>>) when bit_size(padding) in 1..7 do
padding_size = bit_size(padding)
<<padding::size(padding_size)>> = padding
if take_significant_bits(unquote(eos_bits), unquote(eos_bit_count), padding_size) == padding do
<<>>
else
throw({:hpax, {:protocol_error, :invalid_huffman_encoding}})
end
end
## Helpers
@compile {:inline, take_significant_bits: 3}
defp take_significant_bits(value, bit_count, bits_to_take) do
value >>> (bit_count - bits_to_take)
end
end

View File

@@ -0,0 +1,257 @@
( 0) |11111111|11000 1ff8 [13]
( 1) |11111111|11111111|1011000 7fffd8 [23]
( 2) |11111111|11111111|11111110|0010 fffffe2 [28]
( 3) |11111111|11111111|11111110|0011 fffffe3 [28]
( 4) |11111111|11111111|11111110|0100 fffffe4 [28]
( 5) |11111111|11111111|11111110|0101 fffffe5 [28]
( 6) |11111111|11111111|11111110|0110 fffffe6 [28]
( 7) |11111111|11111111|11111110|0111 fffffe7 [28]
( 8) |11111111|11111111|11111110|1000 fffffe8 [28]
( 9) |11111111|11111111|11101010 ffffea [24]
( 10) |11111111|11111111|11111111|111100 3ffffffc [30]
( 11) |11111111|11111111|11111110|1001 fffffe9 [28]
( 12) |11111111|11111111|11111110|1010 fffffea [28]
( 13) |11111111|11111111|11111111|111101 3ffffffd [30]
( 14) |11111111|11111111|11111110|1011 fffffeb [28]
( 15) |11111111|11111111|11111110|1100 fffffec [28]
( 16) |11111111|11111111|11111110|1101 fffffed [28]
( 17) |11111111|11111111|11111110|1110 fffffee [28]
( 18) |11111111|11111111|11111110|1111 fffffef [28]
( 19) |11111111|11111111|11111111|0000 ffffff0 [28]
( 20) |11111111|11111111|11111111|0001 ffffff1 [28]
( 21) |11111111|11111111|11111111|0010 ffffff2 [28]
( 22) |11111111|11111111|11111111|111110 3ffffffe [30]
( 23) |11111111|11111111|11111111|0011 ffffff3 [28]
( 24) |11111111|11111111|11111111|0100 ffffff4 [28]
( 25) |11111111|11111111|11111111|0101 ffffff5 [28]
( 26) |11111111|11111111|11111111|0110 ffffff6 [28]
( 27) |11111111|11111111|11111111|0111 ffffff7 [28]
( 28) |11111111|11111111|11111111|1000 ffffff8 [28]
( 29) |11111111|11111111|11111111|1001 ffffff9 [28]
( 30) |11111111|11111111|11111111|1010 ffffffa [28]
( 31) |11111111|11111111|11111111|1011 ffffffb [28]
' ' ( 32) |010100 14 [ 6]
'!' ( 33) |11111110|00 3f8 [10]
'"' ( 34) |11111110|01 3f9 [10]
'#' ( 35) |11111111|1010 ffa [12]
'$' ( 36) |11111111|11001 1ff9 [13]
'%' ( 37) |010101 15 [ 6]
'&' ( 38) |11111000 f8 [ 8]
''' ( 39) |11111111|010 7fa [11]
'(' ( 40) |11111110|10 3fa [10]
')' ( 41) |11111110|11 3fb [10]
'*' ( 42) |11111001 f9 [ 8]
'+' ( 43) |11111111|011 7fb [11]
',' ( 44) |11111010 fa [ 8]
'-' ( 45) |010110 16 [ 6]
'.' ( 46) |010111 17 [ 6]
'/' ( 47) |011000 18 [ 6]
'0' ( 48) |00000 0 [ 5]
'1' ( 49) |00001 1 [ 5]
'2' ( 50) |00010 2 [ 5]
'3' ( 51) |011001 19 [ 6]
'4' ( 52) |011010 1a [ 6]
'5' ( 53) |011011 1b [ 6]
'6' ( 54) |011100 1c [ 6]
'7' ( 55) |011101 1d [ 6]
'8' ( 56) |011110 1e [ 6]
'9' ( 57) |011111 1f [ 6]
':' ( 58) |1011100 5c [ 7]
';' ( 59) |11111011 fb [ 8]
'<' ( 60) |11111111|1111100 7ffc [15]
'=' ( 61) |100000 20 [ 6]
'>' ( 62) |11111111|1011 ffb [12]
'?' ( 63) |11111111|00 3fc [10]
'@' ( 64) |11111111|11010 1ffa [13]
'A' ( 65) |100001 21 [ 6]
'B' ( 66) |1011101 5d [ 7]
'C' ( 67) |1011110 5e [ 7]
'D' ( 68) |1011111 5f [ 7]
'E' ( 69) |1100000 60 [ 7]
'F' ( 70) |1100001 61 [ 7]
'G' ( 71) |1100010 62 [ 7]
'H' ( 72) |1100011 63 [ 7]
'I' ( 73) |1100100 64 [ 7]
'J' ( 74) |1100101 65 [ 7]
'K' ( 75) |1100110 66 [ 7]
'L' ( 76) |1100111 67 [ 7]
'M' ( 77) |1101000 68 [ 7]
'N' ( 78) |1101001 69 [ 7]
'O' ( 79) |1101010 6a [ 7]
'P' ( 80) |1101011 6b [ 7]
'Q' ( 81) |1101100 6c [ 7]
'R' ( 82) |1101101 6d [ 7]
'S' ( 83) |1101110 6e [ 7]
'T' ( 84) |1101111 6f [ 7]
'U' ( 85) |1110000 70 [ 7]
'V' ( 86) |1110001 71 [ 7]
'W' ( 87) |1110010 72 [ 7]
'X' ( 88) |11111100 fc [ 8]
'Y' ( 89) |1110011 73 [ 7]
'Z' ( 90) |11111101 fd [ 8]
'[' ( 91) |11111111|11011 1ffb [13]
'\' ( 92) |11111111|11111110|000 7fff0 [19]
']' ( 93) |11111111|11100 1ffc [13]
'^' ( 94) |11111111|111100 3ffc [14]
'_' ( 95) |100010 22 [ 6]
'`' ( 96) |11111111|1111101 7ffd [15]
'a' ( 97) |00011 3 [ 5]
'b' ( 98) |100011 23 [ 6]
'c' ( 99) |00100 4 [ 5]
'd' (100) |100100 24 [ 6]
'e' (101) |00101 5 [ 5]
'f' (102) |100101 25 [ 6]
'g' (103) |100110 26 [ 6]
'h' (104) |100111 27 [ 6]
'i' (105) |00110 6 [ 5]
'j' (106) |1110100 74 [ 7]
'k' (107) |1110101 75 [ 7]
'l' (108) |101000 28 [ 6]
'm' (109) |101001 29 [ 6]
'n' (110) |101010 2a [ 6]
'o' (111) |00111 7 [ 5]
'p' (112) |101011 2b [ 6]
'q' (113) |1110110 76 [ 7]
'r' (114) |101100 2c [ 6]
's' (115) |01000 8 [ 5]
't' (116) |01001 9 [ 5]
'u' (117) |101101 2d [ 6]
'v' (118) |1110111 77 [ 7]
'w' (119) |1111000 78 [ 7]
'x' (120) |1111001 79 [ 7]
'y' (121) |1111010 7a [ 7]
'z' (122) |1111011 7b [ 7]
'{' (123) |11111111|1111110 7ffe [15]
'|' (124) |11111111|100 7fc [11]
'}' (125) |11111111|111101 3ffd [14]
'~' (126) |11111111|11101 1ffd [13]
(127) |11111111|11111111|11111111|1100 ffffffc [28]
(128) |11111111|11111110|0110 fffe6 [20]
(129) |11111111|11111111|010010 3fffd2 [22]
(130) |11111111|11111110|0111 fffe7 [20]
(131) |11111111|11111110|1000 fffe8 [20]
(132) |11111111|11111111|010011 3fffd3 [22]
(133) |11111111|11111111|010100 3fffd4 [22]
(134) |11111111|11111111|010101 3fffd5 [22]
(135) |11111111|11111111|1011001 7fffd9 [23]
(136) |11111111|11111111|010110 3fffd6 [22]
(137) |11111111|11111111|1011010 7fffda [23]
(138) |11111111|11111111|1011011 7fffdb [23]
(139) |11111111|11111111|1011100 7fffdc [23]
(140) |11111111|11111111|1011101 7fffdd [23]
(141) |11111111|11111111|1011110 7fffde [23]
(142) |11111111|11111111|11101011 ffffeb [24]
(143) |11111111|11111111|1011111 7fffdf [23]
(144) |11111111|11111111|11101100 ffffec [24]
(145) |11111111|11111111|11101101 ffffed [24]
(146) |11111111|11111111|010111 3fffd7 [22]
(147) |11111111|11111111|1100000 7fffe0 [23]
(148) |11111111|11111111|11101110 ffffee [24]
(149) |11111111|11111111|1100001 7fffe1 [23]
(150) |11111111|11111111|1100010 7fffe2 [23]
(151) |11111111|11111111|1100011 7fffe3 [23]
(152) |11111111|11111111|1100100 7fffe4 [23]
(153) |11111111|11111110|11100 1fffdc [21]
(154) |11111111|11111111|011000 3fffd8 [22]
(155) |11111111|11111111|1100101 7fffe5 [23]
(156) |11111111|11111111|011001 3fffd9 [22]
(157) |11111111|11111111|1100110 7fffe6 [23]
(158) |11111111|11111111|1100111 7fffe7 [23]
(159) |11111111|11111111|11101111 ffffef [24]
(160) |11111111|11111111|011010 3fffda [22]
(161) |11111111|11111110|11101 1fffdd [21]
(162) |11111111|11111110|1001 fffe9 [20]
(163) |11111111|11111111|011011 3fffdb [22]
(164) |11111111|11111111|011100 3fffdc [22]
(165) |11111111|11111111|1101000 7fffe8 [23]
(166) |11111111|11111111|1101001 7fffe9 [23]
(167) |11111111|11111110|11110 1fffde [21]
(168) |11111111|11111111|1101010 7fffea [23]
(169) |11111111|11111111|011101 3fffdd [22]
(170) |11111111|11111111|011110 3fffde [22]
(171) |11111111|11111111|11110000 fffff0 [24]
(172) |11111111|11111110|11111 1fffdf [21]
(173) |11111111|11111111|011111 3fffdf [22]
(174) |11111111|11111111|1101011 7fffeb [23]
(175) |11111111|11111111|1101100 7fffec [23]
(176) |11111111|11111111|00000 1fffe0 [21]
(177) |11111111|11111111|00001 1fffe1 [21]
(178) |11111111|11111111|100000 3fffe0 [22]
(179) |11111111|11111111|00010 1fffe2 [21]
(180) |11111111|11111111|1101101 7fffed [23]
(181) |11111111|11111111|100001 3fffe1 [22]
(182) |11111111|11111111|1101110 7fffee [23]
(183) |11111111|11111111|1101111 7fffef [23]
(184) |11111111|11111110|1010 fffea [20]
(185) |11111111|11111111|100010 3fffe2 [22]
(186) |11111111|11111111|100011 3fffe3 [22]
(187) |11111111|11111111|100100 3fffe4 [22]
(188) |11111111|11111111|1110000 7ffff0 [23]
(189) |11111111|11111111|100101 3fffe5 [22]
(190) |11111111|11111111|100110 3fffe6 [22]
(191) |11111111|11111111|1110001 7ffff1 [23]
(192) |11111111|11111111|11111000|00 3ffffe0 [26]
(193) |11111111|11111111|11111000|01 3ffffe1 [26]
(194) |11111111|11111110|1011 fffeb [20]
(195) |11111111|11111110|001 7fff1 [19]
(196) |11111111|11111111|100111 3fffe7 [22]
(197) |11111111|11111111|1110010 7ffff2 [23]
(198) |11111111|11111111|101000 3fffe8 [22]
(199) |11111111|11111111|11110110|0 1ffffec [25]
(200) |11111111|11111111|11111000|10 3ffffe2 [26]
(201) |11111111|11111111|11111000|11 3ffffe3 [26]
(202) |11111111|11111111|11111001|00 3ffffe4 [26]
(203) |11111111|11111111|11111011|110 7ffffde [27]
(204) |11111111|11111111|11111011|111 7ffffdf [27]
(205) |11111111|11111111|11111001|01 3ffffe5 [26]
(206) |11111111|11111111|11110001 fffff1 [24]
(207) |11111111|11111111|11110110|1 1ffffed [25]
(208) |11111111|11111110|010 7fff2 [19]
(209) |11111111|11111111|00011 1fffe3 [21]
(210) |11111111|11111111|11111001|10 3ffffe6 [26]
(211) |11111111|11111111|11111100|000 7ffffe0 [27]
(212) |11111111|11111111|11111100|001 7ffffe1 [27]
(213) |11111111|11111111|11111001|11 3ffffe7 [26]
(214) |11111111|11111111|11111100|010 7ffffe2 [27]
(215) |11111111|11111111|11110010 fffff2 [24]
(216) |11111111|11111111|00100 1fffe4 [21]
(217) |11111111|11111111|00101 1fffe5 [21]
(218) |11111111|11111111|11111010|00 3ffffe8 [26]
(219) |11111111|11111111|11111010|01 3ffffe9 [26]
(220) |11111111|11111111|11111111|1101 ffffffd [28]
(221) |11111111|11111111|11111100|011 7ffffe3 [27]
(222) |11111111|11111111|11111100|100 7ffffe4 [27]
(223) |11111111|11111111|11111100|101 7ffffe5 [27]
(224) |11111111|11111110|1100 fffec [20]
(225) |11111111|11111111|11110011 fffff3 [24]
(226) |11111111|11111110|1101 fffed [20]
(227) |11111111|11111111|00110 1fffe6 [21]
(228) |11111111|11111111|101001 3fffe9 [22]
(229) |11111111|11111111|00111 1fffe7 [21]
(230) |11111111|11111111|01000 1fffe8 [21]
(231) |11111111|11111111|1110011 7ffff3 [23]
(232) |11111111|11111111|101010 3fffea [22]
(233) |11111111|11111111|101011 3fffeb [22]
(234) |11111111|11111111|11110111|0 1ffffee [25]
(235) |11111111|11111111|11110111|1 1ffffef [25]
(236) |11111111|11111111|11110100 fffff4 [24]
(237) |11111111|11111111|11110101 fffff5 [24]
(238) |11111111|11111111|11111010|10 3ffffea [26]
(239) |11111111|11111111|1110100 7ffff4 [23]
(240) |11111111|11111111|11111010|11 3ffffeb [26]
(241) |11111111|11111111|11111100|110 7ffffe6 [27]
(242) |11111111|11111111|11111011|00 3ffffec [26]
(243) |11111111|11111111|11111011|01 3ffffed [26]
(244) |11111111|11111111|11111100|111 7ffffe7 [27]
(245) |11111111|11111111|11111101|000 7ffffe8 [27]
(246) |11111111|11111111|11111101|001 7ffffe9 [27]
(247) |11111111|11111111|11111101|010 7ffffea [27]
(248) |11111111|11111111|11111101|011 7ffffeb [27]
(249) |11111111|11111111|11111111|1110 ffffffe [28]
(250) |11111111|11111111|11111101|100 7ffffec [27]
(251) |11111111|11111111|11111101|101 7ffffed [27]
(252) |11111111|11111111|11111101|110 7ffffee [27]
(253) |11111111|11111111|11111101|111 7ffffef [27]
(254) |11111111|11111111|11111110|000 7fffff0 [27]
(255) |11111111|11111111|11111011|10 3ffffee [26]
EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]

View File

@@ -0,0 +1,348 @@
defmodule HPAX.Table do
@moduledoc false
@enforce_keys [:max_table_size, :huffman_encoding]
defstruct [
:protocol_max_table_size,
:max_table_size,
:huffman_encoding,
entries: [],
size: 0,
length: 0,
pending_minimum_resize: nil
]
@type huffman_encoding() :: :always | :never
@type t() :: %__MODULE__{
protocol_max_table_size: non_neg_integer(),
max_table_size: non_neg_integer(),
huffman_encoding: huffman_encoding(),
entries: [{binary(), binary()}],
size: non_neg_integer(),
length: non_neg_integer(),
pending_minimum_resize: non_neg_integer() | nil
}
@static_table [
{":authority", nil},
{":method", "GET"},
{":method", "POST"},
{":path", "/"},
{":path", "/index.html"},
{":scheme", "http"},
{":scheme", "https"},
{":status", "200"},
{":status", "204"},
{":status", "206"},
{":status", "304"},
{":status", "400"},
{":status", "404"},
{":status", "500"},
{"accept-charset", nil},
{"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}
]
@static_table_size length(@static_table)
@dynamic_table_start @static_table_size + 1
@doc """
Creates a new HPACK table with the given maximum size.
The maximum size is not the maximum number of entries but rather the maximum size as defined in
http://httpwg.org/specs/rfc7541.html#maximum.table.size.
"""
@spec new(non_neg_integer(), huffman_encoding()) :: t()
def new(protocol_max_table_size, huffman_encoding)
when is_integer(protocol_max_table_size) and protocol_max_table_size >= 0 and
huffman_encoding in [:always, :never] do
%__MODULE__{
protocol_max_table_size: protocol_max_table_size,
max_table_size: protocol_max_table_size,
huffman_encoding: huffman_encoding
}
end
@doc """
Adds the given header to the given table.
If the new entry does not fit within the max table size then the oldest entries will be evicted.
Header names should be lowercase when added to the HPACK table
as per the [HTTP/2 spec](https://http2.github.io/http2-spec/#rfc.section.8.1.2):
> header field names MUST be converted to lowercase prior to their encoding in HTTP/2
"""
@spec add(t(), binary(), binary()) :: t()
def add(%__MODULE__{} = table, name, value) do
%{max_table_size: max_table_size, size: size} = table
entry_size = entry_size(name, value)
cond do
# An attempt to add an entry larger than the maximum size causes the table to be emptied of
# all existing entries and results in an empty table.
entry_size > max_table_size ->
%{table | entries: [], size: 0, length: 0}
size + entry_size > max_table_size ->
table
|> evict_to_size(max_table_size - entry_size)
|> add_header(name, value, entry_size)
true ->
add_header(table, name, value, entry_size)
end
end
defp add_header(%__MODULE__{} = table, name, value, entry_size) do
%{entries: entries, size: size, length: length} = table
%{table | entries: [{name, value} | entries], size: size + entry_size, length: length + 1}
end
@doc """
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).
"""
@spec lookup_by_index(t(), pos_integer()) :: {:ok, {binary(), binary() | nil}} | :error
def lookup_by_index(table, index)
# Static table
for {header, index} <- Enum.with_index(@static_table, 1) do
def lookup_by_index(%__MODULE__{}, unquote(index)), do: {:ok, unquote(header)}
end
def lookup_by_index(%__MODULE__{length: 0}, _index) do
:error
end
def lookup_by_index(%__MODULE__{entries: entries, length: length}, index)
when index >= @dynamic_table_start and index <= @dynamic_table_start + length - 1 do
{:ok, Enum.at(entries, index - @dynamic_table_start)}
end
def lookup_by_index(%__MODULE__{}, _index) do
:error
end
@doc """
Looks up the index of a header by its name and value.
It returns:
* `{:full, index}` if the full header (name and value) are present in the table at `index`
* `{:name, index}` if `name` is present in the table but with a different value than `value`
* `:not_found` if the header name is not in the table at all
Header names should be lowercase when looked up in the HPACK table
as per the [HTTP/2 spec](https://http2.github.io/http2-spec/#rfc.section.8.1.2):
> header field names MUST be converted to lowercase prior to their encoding in HTTP/2
"""
@spec lookup_by_header(t(), binary(), binary() | nil) ::
{:full, pos_integer()} | {:name, pos_integer()} | :not_found
def lookup_by_header(table, name, value)
def lookup_by_header(%__MODULE__{entries: entries}, name, value) do
case static_lookup_by_header(name, value) do
{:full, _index} = result ->
result
{:name, index} ->
# Check if we get full match in the dynamic tabble
case dynamic_lookup_by_header(entries, name, value, @dynamic_table_start, nil) do
{:full, _index} = result -> result
_other -> {:name, index}
end
:not_found ->
dynamic_lookup_by_header(entries, name, value, @dynamic_table_start, nil)
end
end
for {{name, value}, index} when is_binary(value) <- Enum.with_index(@static_table, 1) do
defp static_lookup_by_header(unquote(name), unquote(value)) do
{:full, unquote(index)}
end
end
static_table_names =
@static_table
|> Enum.map(&elem(&1, 0))
|> Enum.with_index(1)
|> Enum.uniq_by(&elem(&1, 0))
for {name, index} <- static_table_names do
defp static_lookup_by_header(unquote(name), _value) do
{:name, unquote(index)}
end
end
defp static_lookup_by_header(_name, _value) do
:not_found
end
defp dynamic_lookup_by_header([{name, value} | _rest], name, value, index, _name_index) do
{:full, index}
end
defp dynamic_lookup_by_header([{name, _} | rest], name, value, index, _name_index) do
dynamic_lookup_by_header(rest, name, value, index + 1, index)
end
defp dynamic_lookup_by_header([_other | rest], name, value, index, name_index) do
dynamic_lookup_by_header(rest, name, value, index + 1, name_index)
end
defp dynamic_lookup_by_header([], _name, _value, _index, name_index) do
if name_index, do: {:name, name_index}, else: :not_found
end
@doc """
Changes the table's protocol negotiated maximum size, possibly evicting entries as needed to satisfy.
If the indicated size is less than the table's current max size, entries
will be evicted as needed to fit within the specified size, and the table's
maximum size will be decreased to the specified value. An will also be
set which will enqueue a 'dynamic table size update' command to be prefixed
to the next block encoded with this table, per RFC9113§4.3.1.
If the indicated size is greater than or equal to the table's current max size, no entries are evicted
and the table's maximum size changes to the specified value.
In all cases, the table's `:protocol_max_table_size` is updated accordingly
"""
@spec resize(t(), non_neg_integer()) :: t()
def resize(%__MODULE__{} = table, new_protocol_max_table_size) do
pending_minimum_resize =
case table.pending_minimum_resize do
nil -> new_protocol_max_table_size
current -> min(current, new_protocol_max_table_size)
end
%{
evict_to_size(table, new_protocol_max_table_size)
| protocol_max_table_size: new_protocol_max_table_size,
max_table_size: new_protocol_max_table_size,
pending_minimum_resize: pending_minimum_resize
}
end
def dynamic_resize(%__MODULE__{} = table, new_max_table_size) do
%{
evict_to_size(table, new_max_table_size)
| max_table_size: new_max_table_size
}
end
@doc """
Returns (and clears) any pending resize events on the table which will need to be signalled to
the decoder via dynamic table size update messages. Intended to be called at the start of any
block encode to prepend such dynamic table size update(s) as needed. The value of
`pending_minimum_resize` indicates the smallest maximum size of this table which has not yet
been signalled to the decoder, and is always included in the list returned if it is set.
Additionally, if the current max table size is larger than this value, it is also included int
the list, per https://www.rfc-editor.org/rfc/rfc7541#section-4.2
"""
def pop_pending_resizes(%__MODULE__{pending_minimum_resize: nil} = table), do: {table, []}
def pop_pending_resizes(%__MODULE__{} = table) do
pending_resizes =
if table.max_table_size > table.pending_minimum_resize,
do: [table.pending_minimum_resize, table.max_table_size],
else: [table.pending_minimum_resize]
{%{table | pending_minimum_resize: nil}, pending_resizes}
end
# Removes records as necessary to have the total size of entries within the table be less than
# or equal to the specified value. Does not change the table's max size.
defp evict_to_size(%__MODULE__{size: size} = table, new_size) when size <= new_size, do: table
defp evict_to_size(%__MODULE__{entries: entries, size: size} = table, new_size) do
{new_entries_reversed, new_size} =
evict_towards_size(Enum.reverse(entries), size, new_size)
%{
table
| entries: Enum.reverse(new_entries_reversed),
size: new_size,
length: length(new_entries_reversed)
}
end
defp evict_towards_size([{name, value} | rest], size, max_target_size) do
new_size = size - entry_size(name, value)
if new_size <= max_target_size do
{rest, new_size}
else
evict_towards_size(rest, new_size, max_target_size)
end
end
defp evict_towards_size([], 0, _max_target_size) do
{[], 0}
end
defp entry_size(name, value) do
byte_size(name) + byte_size(value) + 32
end
# Made public to be used in tests.
@doc false
def __static_table__() do
@static_table
end
end

View File

@@ -0,0 +1,89 @@
defmodule HPAX.Types do
@moduledoc false
import Bitwise, only: [<<<: 2]
alias HPAX.Huffman
# This is used as a macro and not an inlined function because we want to be able to use it in
# guards.
defmacrop power_of_two(n) do
quote do: 1 <<< unquote(n)
end
## Encoding
@spec encode_integer(non_neg_integer(), 1..8) :: bitstring()
def encode_integer(integer, prefix)
def encode_integer(integer, prefix) when integer < power_of_two(prefix) - 1 do
<<integer::size(prefix)>>
end
def encode_integer(integer, prefix) do
initial = power_of_two(prefix) - 1
remaining = integer - initial
<<initial::size(prefix), encode_remaining_integer(remaining)::binary>>
end
defp encode_remaining_integer(remaining) when remaining >= 128 do
first = rem(remaining, 128) + 128
<<first::8, encode_remaining_integer(div(remaining, 128))::binary>>
end
defp encode_remaining_integer(remaining) do
<<remaining::8>>
end
@spec encode_binary(binary(), boolean()) :: iodata()
def encode_binary(binary, huffman?) do
binary = if huffman?, do: Huffman.encode(binary), else: binary
huffman_bit = if huffman?, do: 1, else: 0
binary_size = encode_integer(byte_size(binary), 7)
[<<huffman_bit::1, binary_size::bitstring>>, binary]
end
## Decoding
@spec decode_integer(bitstring, 1..8) :: {:ok, non_neg_integer(), binary()} | :error
def decode_integer(bitstring, prefix) when is_bitstring(bitstring) and prefix in 1..8 do
with <<value::size(prefix), rest::binary>> <- bitstring do
if value < power_of_two(prefix) - 1 do
{:ok, value, rest}
else
decode_remaining_integer(rest, value, 0)
end
else
_ -> :error
end
end
defp decode_remaining_integer(<<0::1, value::7, rest::binary>>, int, m) do
{:ok, int + (value <<< m), rest}
end
defp decode_remaining_integer(<<1::1, value::7, rest::binary>>, int, m) do
decode_remaining_integer(rest, int + (value <<< m), m + 7)
end
defp decode_remaining_integer(_, _, _) do
:error
end
@spec decode_binary(binary) :: {:ok, binary(), binary()} | :error
def decode_binary(binary) when is_binary(binary) do
with <<huffman_bit::1, rest::bitstring>> <- binary,
{:ok, length, rest} <- decode_integer(rest, 7),
<<contents::binary-size(length), rest::binary>> <- rest do
contents =
case huffman_bit do
0 -> contents
1 -> Huffman.decode(contents)
end
{:ok, contents, rest}
else
_ -> :error
end
end
end

54
phoenix/deps/hpax/mix.exs Normal file
View File

@@ -0,0 +1,54 @@
defmodule HPAX.MixProject do
use Mix.Project
@version "1.0.3"
@repo_url "https://github.com/elixir-mint/hpax"
def project do
[
app: :hpax,
version: @version,
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps(),
# Tests
test_coverage: [tool: ExCoveralls],
# Hex
package: package(),
description: "Implementation of the HPACK protocol (RFC 7541) for Elixir",
# Docs
name: "HPAX",
docs: [
source_ref: "v#{@version}",
source_url: @repo_url
]
]
end
def application do
[
extra_applications: []
]
end
defp deps do
[
{:ex_doc, "~> 0.34", only: :dev},
{:hpack, ">= 0.0.0", hex: :hpack_erl, only: :test},
{:stream_data, "~> 1.0", only: [:dev, :test]},
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false},
{:excoveralls, "~> 0.18", only: :test},
{:castore, "~> 1.0", only: :test}
]
end
defp package do
[
licenses: ["Apache-2.0"],
links: %{"GitHub" => @repo_url}
]
end
end