diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..25cc671 --- /dev/null +++ b/.clang-format @@ -0,0 +1,7 @@ +BasedOnStyle: Google +ColumnLimit : 110 +AllowShortFunctionsOnASingleLine: false +AllowShortLambdasOnASingleLine: true +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +SortIncludes: false \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..184e9a6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: [kelbon] + diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..856e514 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,37 @@ +name: build_and_test + +on: push + +jobs: + build: + strategy: + matrix: + os: [ubuntu-22.04] + compiler: [g++-12, clang++-14] + cpp_standard: [20] + build_type: [Debug, Release] + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v2 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install ninja-build lld gcc-12 clang-14 + sudo ln -sf /usr/local/bin/ld /usr/bin/lld + - name: Configure CMake + run: | + cmake . -DHPACK_ENABLE_TESTING=ON \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_CXX_COMPILER=${{matrix.compiler}} \ + -DCMAKE_CXX_STANDARD=${{matrix.cpp_standard}} \ + -B build -G "Ninja" + - name: Build + run: + cmake --build build + + - name: Test + run: | + cd build + ctest --output-on-failure -C ${{matrix.build_type}} -V diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa8b66f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/build* +/.cache +/.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9236ad2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.05) +project(HPACK LANGUAGES CXX) + +### options ### + +option(HPACK_ENABLE_TESTING "enables testing" OFF) + +### dependecies ### + +include(cmake/get_cpm.cmake) + +set(BOOST_INCLUDE_LIBRARIES intrusive) +CPMAddPackage( + NAME Boost + VERSION 1.84.0 + URL https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.xz + OPTIONS "BOOST_ENABLE_CMAKE ON" +) +unset(BOOST_INCLUDE_LIBRARIES) +find_package(Boost 1.84 COMPONENTS intrusive REQUIRED) + +### hpacklib ### + +add_library(hpacklib STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/decoder.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/dynamic_table.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/huffman.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/static_table.cpp") + +target_include_directories(hpacklib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") + +target_link_libraries(hpacklib PUBLIC Boost::intrusive) + +set_target_properties(hpacklib PROPERTIES + CMAKE_CXX_EXTENSIONS OFF + LINKER_LANGUAGE CXX + CMAKE_CXX_STANDARD_REQUIRED ON + CXX_STANDARD 20) + +if(HPACK_ENABLE_TESTING) + include(CTest) + add_subdirectory(tests) +endif() diff --git a/cmake/get_cpm.cmake b/cmake/get_cpm.cmake new file mode 100644 index 0000000..baf2d8c --- /dev/null +++ b/cmake/get_cpm.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.40.2) +set(CPM_HASH_SUM "c8cdc32c03816538ce22781ed72964dc864b2a34a310d3b7104812a5ca2d835d") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/include/hpack/basic_types.hpp b/include/hpack/basic_types.hpp new file mode 100644 index 0000000..54a44e8 --- /dev/null +++ b/include/hpack/basic_types.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include + +#ifndef KELBON_HPACK_HANDLE_PROTOCOL_ERROR + +#include + +namespace hpack { +struct protocol_error : std::exception {}; +} // namespace hpack + +#define KELBON_HPACK_HANDLE_PROTOCOL_ERROR \ + throw ::hpack::protocol_error { \ + } +#endif + +namespace hpack { + +struct sym_info_t { + uint32_t bits; + uint8_t bit_count; +}; +extern const sym_info_t huffman_table[257]; + +// uint16_t(-1) if not found +uint16_t huffman_decode_table_find(sym_info_t info); + +// integer/string len +using size_type = uint32_t; +// header index +using index_type = uint32_t; +using byte_t = unsigned char; +using In = const byte_t*; + +template +concept Out = std::output_iterator; + +[[noreturn]] inline void handle_protocol_error() { + KELBON_HPACK_HANDLE_PROTOCOL_ERROR; +} +[[noreturn]] inline void handle_size_error() { + KELBON_HPACK_HANDLE_PROTOCOL_ERROR; +} + +namespace noexport { + +template +struct adapted_output_iterator { + T base_it; + mutable byte_t byte = 0; + + using iterator_category = std::output_iterator_tag; + using value_type = byte_t; + using difference_type = std::ptrdiff_t; + + constexpr value_type& operator*() const noexcept { + return byte; + } + constexpr adapted_output_iterator& operator++() { + *base_it = byte; + ++base_it; + return *this; + } + constexpr adapted_output_iterator operator++(int) { + auto cpy = *this; + ++(*this); + return cpy; + } +}; + +template +auto adapt_output_iterator(O it) { + return adapted_output_iterator(it); +} +template +auto adapt_output_iterator(adapted_output_iterator it) { + return it; +} + +inline byte_t* adapt_output_iterator(byte_t* ptr) { + return ptr; +} +inline byte_t* adapt_output_iterator(std::byte* ptr) { + return reinterpret_cast(ptr); +} +inline byte_t* adapt_output_iterator(char* ptr) { + return reinterpret_cast(ptr); +} + +template +Original unadapt(adapted_output_iterator it) { + return Original(std::move(it.base_it)); +} + +template +Original unadapt(byte_t* ptr) { + static_assert(std::is_pointer_v); + return reinterpret_cast(ptr); +} + +} // namespace noexport + +struct table_entry { + std::string_view name; // empty if not found + std::string_view value; // empty if no + + constexpr explicit operator bool() const noexcept { + return !name.empty(); + } + auto operator<=>(const table_entry&) const = default; +}; + +struct find_result_t { + // not found by default + index_type header_name_index = 0; + bool value_indexed = false; + + constexpr explicit operator bool() const noexcept { + return header_name_index != 0; + } +}; + +} // namespace hpack diff --git a/include/hpack/decoder.hpp b/include/hpack/decoder.hpp new file mode 100644 index 0000000..0df766b --- /dev/null +++ b/include/hpack/decoder.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include "hpack/basic_types.hpp" +#include "hpack/dynamic_table.hpp" + +namespace hpack { + +struct decoded_string { + private: + const char* data = nullptr; + size_type sz = 0; + uint8_t allocated_sz_log2 = 0; // != 0 after decoding huffman str + + friend void decode_string(In&, In, decoded_string&); + + void set_huffman(const char* ptr, size_type len); + + public: + decoded_string() = default; + + decoded_string(const char* ptr, size_type len, bool is_huffman_encoded) : data(ptr), sz(len) { + if (!is_huffman_encoded) + return; + set_huffman(ptr, len); + } + + // precondition: str.size() less then max of size_type + decoded_string(std::string_view str, bool is_huffman_encoded) + : decoded_string(str.data(), str.size(), is_huffman_encoded) { + assert(std::in_range(str.size())); + } + + decoded_string(decoded_string&& other) noexcept { + swap(other); + } + + decoded_string& operator=(decoded_string&& other) noexcept { + swap(other); + return *this; + } + + // not huffman encoded string + decoded_string& operator=(std::string_view str) noexcept { + assert(std::in_range(str.size())); + reset(); + data = str.data(); + sz = str.size(); + return *this; + } + + void swap(decoded_string& other) noexcept { + std::swap(data, other.data); + std::swap(sz, other.sz); + std::swap(allocated_sz_log2, other.allocated_sz_log2); + } + + friend void swap(decoded_string& l, decoded_string& r) noexcept { + l.swap(r); + } + + ~decoded_string() { + reset(); + } + + void reset() noexcept { + if (allocated_sz_log2) + free((void*)data); + data = nullptr; + sz = 0; + allocated_sz_log2 = 0; + } + + [[nodiscard]] size_t bytes_allocated() const noexcept { + if (!allocated_sz_log2) + return 0; + return 1 << allocated_sz_log2; + } + + [[nodiscard]] std::string_view str() const noexcept { + return std::string_view(data, sz); + } + + // true if not empty + explicit operator bool() const noexcept { + return sz != 0; + } + + bool operator==(const decoded_string& other) const noexcept { + return str() == other.str(); + } + bool operator==(std::string_view other) const noexcept { + return str() == other; + } + + std::strong_ordering operator<=>(const decoded_string& other) const noexcept { + return str() <=> other.str(); + } + + std::strong_ordering operator<=>(std::string_view other) const noexcept { + return str() <=> other; + } +}; + +// note: decoding next header invalidates previous header +struct header_view { + decoded_string name; + decoded_string value; + + // header may be not present if default contructed or table_size_update happen instead of header + explicit operator bool() const noexcept { + return name || value; + } + + header_view& operator=(header_view&&) = default; + header_view& operator=(table_entry entry) { + name = entry.name; + value = entry.value; + return *this; + } +}; + +void decode_string(In& in, In e, decoded_string& out); + +struct decoder { + dynamic_table_t dyntab; + + // 4096 - default size in HTTP/2 + explicit decoder(size_type max_dyntab_size = 4096, + std::pmr::memory_resource* resource = std::pmr::get_default_resource()) + : dyntab(max_dyntab_size, resource) { + } + + decoder(decoder&&) = default; + decoder& operator=(decoder&&) noexcept = default; + /* + Note: this function ignores special 'cookie' header case + https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.5 + and protocol error if decoded header name is not lowercase + */ + void decode_header(In& in, In e, header_view& out); + + // returns status code + int decode_response_status(In& in, In e); +}; + +} // namespace hpack diff --git a/include/hpack/dynamic_table.hpp b/include/hpack/dynamic_table.hpp new file mode 100644 index 0000000..d57c38a --- /dev/null +++ b/include/hpack/dynamic_table.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include + +#include + +#include "hpack/basic_types.hpp" +#include "hpack/static_table.hpp" + +namespace hpack { + +namespace bi = boost::intrusive; + +struct dynamic_table_t { + struct entry_t; + + private: + struct key_of_entry { + using type = table_entry; + table_entry operator()(const entry_t& v) const noexcept; + }; + // for forward declaring entry_t + using hook_type_option = bi::base_hook>>; + + // invariant: do not contain nullptrs + std::vector entries; + bi::multiset, hook_type_option, bi::key_of_value> set; + // in bytes + // invariant: <= _max_size + size_type _current_size = 0; + size_type _max_size = 0; + size_t _insert_count = 0; + // invariant: != nullptr + std::pmr::memory_resource* _resource = std::pmr::get_default_resource(); + /* + <---------- Index Address Space ----------> + <-- Static Table --> <-- Dynamic Table --> + +---+-----------+---+ +---+-----------+---+ + | 1 | ... | s | |s+1| ... |s+k| + +---+-----------+---+ +---+-----------+---+ + ^ | + | V + Insertion Point Dropping Point + */ + public: + dynamic_table_t() = default; + explicit dynamic_table_t(size_type max_size, + std::pmr::memory_resource* m = std::pmr::get_default_resource()) noexcept; + + dynamic_table_t(const dynamic_table_t&) = delete; + + dynamic_table_t(dynamic_table_t&& other) noexcept; + + void operator=(const dynamic_table_t&) = delete; + + dynamic_table_t& operator=(dynamic_table_t&& other) noexcept; + + ~dynamic_table_t(); + + // returns index of added pair, 0 if cannot add + index_type add_entry(std::string_view name, std::string_view value); + + size_type current_size() const noexcept { + return _current_size; + } + size_type max_size() const noexcept { + return _max_size; + } + + void update_size(size_type new_max_size); + + // min value is static_table_t::first_unused_index + index_type current_max_index() const noexcept { + return entries.size() + static_table_t::first_unused_index; + } + + find_result_t find(std::string_view name, std::string_view value) noexcept; + find_result_t find(index_type name, std::string_view value) noexcept; + + // precondition: first_unused_index <= index <= current_max_index() + // Note: returned value may be invalidated on next .add_entry() + table_entry get_entry(index_type index) const noexcept; + + void reset() noexcept; + std::pmr::memory_resource* get_resource() const noexcept { + return _resource; + } + + private: + // precondition: bytes <= _max_size + void evict_until_fits_into(size_type bytes) noexcept; + // precondition: entry now in 'entries' + index_type indexof(const entry_t& e) const noexcept; +}; + +// searches in both static and dynamic tables +// dyntab is used only if required (index >= 62) +[[nodiscard]] inline table_entry get_by_index(index_type header_index, dynamic_table_t* dyntab) { + /* + Indices strictly greater than the sum of the lengths of both tables + MUST be treated as a decoding error. + */ + if (header_index == 0) [[unlikely]] + handle_protocol_error(); + if (header_index < static_table_t::first_unused_index) + return static_table_t::get_entry(header_index); + if (header_index > dyntab->current_max_index()) [[unlikely]] + handle_protocol_error(); + return dyntab->get_entry(header_index); +} + +} // namespace hpack diff --git a/include/hpack/encoder.hpp b/include/hpack/encoder.hpp new file mode 100644 index 0000000..381dea0 --- /dev/null +++ b/include/hpack/encoder.hpp @@ -0,0 +1,254 @@ +#pragma once + +#include "hpack/dynamic_table.hpp" +#include "hpack/strings.hpp" +#include "hpack/integers.hpp" + +namespace hpack { + +struct encoder { + dynamic_table_t dyntab; + + // 4096 - default size in HTTP/2 + explicit encoder(size_type max_dyntab_size = 4096, + std::pmr::memory_resource* resource = std::pmr::get_default_resource()) + : dyntab(max_dyntab_size, resource) { + } + + encoder(encoder&&) = default; + encoder& operator=(encoder&&) noexcept = default; + + // indexed name and value, for example ":path" "/index.html" from static table + // or some index from dynamic table + template + O encode_header_fully_indexed(index_type header_index, O _out) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | Index (7+) | + +---+---------------------------+ + */ + assert(header_index <= dyntab.current_max_index()); + auto out = noexport::adapt_output_iterator(_out); + // indexed name and value 0b1... + *out = 0b1000'0000; + return noexport::unadapt(encode_integer(header_index, 7, out)); + } + + // only name indexed + // precondition: header_index present in static or dynamic table + template + O encode_header_and_cache(index_type header_index, std::string_view value, O _out) { + assert(header_index <= dyntab.current_max_index() && header_index != 0); + auto out = noexport::adapt_output_iterator(_out); + // indexed name, new value 0b01... + *out = 0b0100'0000; + out = encode_integer(header_index, 6, out); + std::string_view str = get_by_index(header_index, &dyntab).name; + dyntab.add_entry(str, value); + return noexport::unadapt(encode_string(value, out)); + } + + // indexes value for future use + // 'out_index' contains index of 'name' + 'value' pair after encode + template + O encode_header_and_cache(std::string_view name, std::string_view value, O _out) { + /* + new name + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | 0 | + +---+---+-----------------------+ + | H | Name Length (7+) | + +---+---------------------------+ + | Name String (Length octets) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length octets) | + +-------------------------------+ + */ + auto out = noexport::adapt_output_iterator(_out); + *out = 0b0100'0000; + ++out; + out = encode_string(name, out); + dyntab.add_entry(name, value); + return noexport::unadapt(encode_string(value, out)); + } + + template + O encode_header_without_indexing(index_type name, std::string_view value, O _out) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 0 | Index (4+) | + +---+---+-----------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length octets) | + +-------------------------------+ + */ + assert(name <= dyntab.current_max_index()); + auto out = noexport::adapt_output_iterator(_out); + *out = 0; + out = encode_integer(name, 4, out); + return noexport::unadapt(encode_string(value, out)); + } + + template + O encode_header_without_indexing(std::string_view name, std::string_view value, O _out) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 0 | 0 | + +---+---+-----------------------+ + | H | Name Length (7+) | + +---+---------------------------+ + | Name String (Length octets) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length octets) | + +-------------------------------+ + */ + auto out = noexport::adapt_output_iterator(_out); + *out = 0; + ++out; + out = encode_string(name, out); + return noexport::unadapt(encode_string(value, out)); + } + + // same as without_indexing, but should not be stored in any proxy memory etc + template + O encode_header_never_indexing(index_type name, std::string_view value, O _out) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 1 | Index (4+) | + +---+---+-----------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length octets) | + +-------------------------------+ + */ + assert(name <= dyntab.current_max_index()); + auto out = noexport::adapt_output_iterator(_out); + *out = 0b0001'0000; + out = encode_integer(name, 4, out); + return noexport::unadapt(encode_string(value, out)); + } + + template + O encode_header_never_indexing(std::string_view name, std::string_view value, O _out) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 1 | 0 | + +---+---+-----------------------+ + | H | Name Length (7+) | + +---+---------------------------+ + | Name String (Length octets) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length octets) | + +-------------------------------+ + */ + auto out = noexport::adapt_output_iterator(_out); + *out = 0b0001'0000; + ++out; + out = encode_string(name, out); + return noexport::unadapt(encode_string(value, out)); + } + + /* + default encode, more calculations, less memory + minimizes size of encoded + + usually its better to encode headers manually, but may be used as "okay somehow encode" + + 'Cache' - if true, then will cache headers if they are not in cache yet + 'Huffman' - use Huffman encoding for strings or no (prefer no) + + Note: static table has priority over dynamic table + (eg indexed name which is indexed in both tables uses index from static, + same for name + value pairs) + */ + template + O encode(std::string_view name, std::string_view value, O out) { + find_result_t r2 = static_table_t::find(name, value); + if (r2.value_indexed) + return encode_header_fully_indexed(r2.header_name_index, out); + find_result_t r1 = dyntab.find(name, value); + if (r1.value_indexed) + return encode_header_fully_indexed(r1.header_name_index, out); + if (r2) { + if constexpr (Cache) + return encode_header_and_cache(r2.header_name_index, value, out); + else + return encode_header_without_indexing(r2.header_name_index, value, out); + } + if (r1) { + if constexpr (Cache) + return encode_header_and_cache(r1.header_name_index, value, out); + else + return encode_header_without_indexing(r1.header_name_index, value, out); + } + if constexpr (Cache) + return encode_header_and_cache(name, value, out); + else + return encode_header_without_indexing(name, value, out); + } + + template + O encode(index_type name, std::string_view value, O out) { + find_result_t r2 = static_table_t::find(name, value); + if (r2.value_indexed) + return encode_header_fully_indexed(r2.header_name_index, out); + find_result_t r1 = dyntab.find(name, value); + if (r1.value_indexed) + return encode_header_fully_indexed(r1.header_name_index, out); + if (r2) { + if constexpr (Cache) + return encode_header_and_cache(r2.header_name_index, value, out); + else + return encode_header_without_indexing(r2.header_name_index, value, out); + } + if (r1) { + if constexpr (Cache) + return encode_header_and_cache(r1.header_name_index, value, out); + else + return encode_header_without_indexing(r1.header_name_index, value, out); + } + if constexpr (Cache) + return encode_header_and_cache(name, value, out); + else + return encode_header_without_indexing(name, value, out); + } + + /* + An encoder can choose to use less capacity than this maximum size + (see Section 6.3), but the chosen size MUST stay lower than or equal + to the maximum set by the protocol. + + A change in the maximum size of the dynamic table is signaled via a + dynamic table size update (see Section 6.3). This dynamic table size + update MUST occur at the beginning of the first header block + following the change to the dynamic table size. In HTTP/2, this + follows a settings acknowledgment (see Section 6.5.3 of [HTTP2]). + */ + template + O encode_dynamic_table_size_update(size_type new_size, O _out) noexcept { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | Max size (5+) | + +---+---------------------------+ + */ + auto out = noexport::adapt_output_iterator(_out); + *out = 0b0010'0000; + return noexport::unadapt(encode_integer(new_size, 5, out)); + } +}; + +} // namespace hpack diff --git a/include/hpack/hpack.hpp b/include/hpack/hpack.hpp new file mode 100644 index 0000000..e5fc307 --- /dev/null +++ b/include/hpack/hpack.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "hpack/encoder.hpp" +#include "hpack/decoder.hpp" + +namespace hpack { + +// if 'Cache' is true, then tries to use dynamic table for indexing header +// and reduce size in next decoding +// if 'HUffman' is true, then strings will be encoded with huffman encoding +template +O encode_headers_block(encoder& enc, auto&& range_of_headers, O out) { + for (auto&& [name, value] : range_of_headers) + out = enc.template encode(name, value, out); + return out; +} + +// visitor should accept two string_views, name and value +// ignores (may be handled by caller side): +// * special case Cookie header separated by key-value pairs +// * possibly not lowercase headers (it should be protocol error, +// but nothing breaks anyway, useless check) +template +V decode_headers_block(decoder& dec, std::span bytes, V visitor) { + const auto* in = bytes.data(); + const auto* e = in + bytes.size(); + header_view header; + while (in != e) { + dec.decode_header(in, e, header); + if (header) // dynamic size update decoded without error + visitor(header.name.str(), header.value.str()); + } + return visitor; +} + +} // namespace hpack diff --git a/include/hpack/huffman_table.def b/include/hpack/huffman_table.def new file mode 100644 index 0000000..8d9656f --- /dev/null +++ b/include/hpack/huffman_table.def @@ -0,0 +1,263 @@ +#ifndef HUFFMAN_TABLE +#error HUFFMAN_TABLE(code, bits, count_bits) should be defined +#endif + +HUFFMAN_TABLE(0, 1111111111000, 13) +HUFFMAN_TABLE(1, 11111111111111111011000, 23) +HUFFMAN_TABLE(2, 1111111111111111111111100010, 28) +HUFFMAN_TABLE(3, 1111111111111111111111100011, 28) +HUFFMAN_TABLE(4, 1111111111111111111111100100, 28) +HUFFMAN_TABLE(5, 1111111111111111111111100101, 28) +HUFFMAN_TABLE(6, 1111111111111111111111100110, 28) +HUFFMAN_TABLE(7, 1111111111111111111111100111, 28) +HUFFMAN_TABLE(8, 1111111111111111111111101000, 28) +HUFFMAN_TABLE(9, 111111111111111111101010, 24) +HUFFMAN_TABLE(10, 111111111111111111111111111100, 30) +HUFFMAN_TABLE(11, 1111111111111111111111101001, 28) +HUFFMAN_TABLE(12, 1111111111111111111111101010, 28) +HUFFMAN_TABLE(13, 111111111111111111111111111101, 30) +HUFFMAN_TABLE(14, 1111111111111111111111101011, 28) +HUFFMAN_TABLE(15, 1111111111111111111111101100, 28) +HUFFMAN_TABLE(16, 1111111111111111111111101101, 28) +HUFFMAN_TABLE(17, 1111111111111111111111101110, 28) +HUFFMAN_TABLE(18, 1111111111111111111111101111, 28) +HUFFMAN_TABLE(19, 1111111111111111111111110000, 28) +HUFFMAN_TABLE(20, 1111111111111111111111110001, 28) +HUFFMAN_TABLE(21, 1111111111111111111111110010, 28) +HUFFMAN_TABLE(22, 111111111111111111111111111110, 30) +HUFFMAN_TABLE(23, 1111111111111111111111110011, 28) +HUFFMAN_TABLE(24, 1111111111111111111111110100, 28) +HUFFMAN_TABLE(25, 1111111111111111111111110101, 28) +HUFFMAN_TABLE(26, 1111111111111111111111110110, 28) +HUFFMAN_TABLE(27, 1111111111111111111111110111, 28) +HUFFMAN_TABLE(28, 1111111111111111111111111000, 28) +HUFFMAN_TABLE(29, 1111111111111111111111111001, 28) +HUFFMAN_TABLE(30, 1111111111111111111111111010, 28) +HUFFMAN_TABLE(31, 1111111111111111111111111011, 28) +HUFFMAN_TABLE(32, 010100, 6) +HUFFMAN_TABLE(33, 1111111000, 10) +HUFFMAN_TABLE(34, 1111111001, 10) +HUFFMAN_TABLE(35, 111111111010, 12) +HUFFMAN_TABLE(36, 1111111111001, 13) +HUFFMAN_TABLE(37, 010101, 6) +HUFFMAN_TABLE(38, 11111000, 8) +HUFFMAN_TABLE(39, 11111111010, 11) +HUFFMAN_TABLE(40, 1111111010, 10) +HUFFMAN_TABLE(41, 1111111011, 10) +HUFFMAN_TABLE(42, 11111001, 8) +HUFFMAN_TABLE(43, 11111111011, 11) +HUFFMAN_TABLE(44, 11111010, 8) +HUFFMAN_TABLE(45, 010110, 6) +HUFFMAN_TABLE(46, 010111, 6) +HUFFMAN_TABLE(47, 011000, 6) +HUFFMAN_TABLE(48, 00000, 5) +HUFFMAN_TABLE(49, 00001, 5) +HUFFMAN_TABLE(50, 00010, 5) +HUFFMAN_TABLE(51, 011001, 6) +HUFFMAN_TABLE(52, 011010, 6) +HUFFMAN_TABLE(53, 011011, 6) +HUFFMAN_TABLE(54, 011100, 6) +HUFFMAN_TABLE(55, 011101, 6) +HUFFMAN_TABLE(56, 011110, 6) +HUFFMAN_TABLE(57, 011111, 6) +HUFFMAN_TABLE(58, 1011100, 7) +HUFFMAN_TABLE(59, 11111011, 8) +HUFFMAN_TABLE(60, 111111111111100, 15) +HUFFMAN_TABLE(61, 100000, 6) +HUFFMAN_TABLE(62, 111111111011, 12) +HUFFMAN_TABLE(63, 1111111100, 10) +HUFFMAN_TABLE(64, 1111111111010, 13) +HUFFMAN_TABLE(65, 100001, 6) +HUFFMAN_TABLE(66, 1011101, 7) +HUFFMAN_TABLE(67, 1011110, 7) +HUFFMAN_TABLE(68, 1011111, 7) +HUFFMAN_TABLE(69, 1100000, 7) +HUFFMAN_TABLE(70, 1100001, 7) +HUFFMAN_TABLE(71, 1100010, 7) +HUFFMAN_TABLE(72, 1100011, 7) +HUFFMAN_TABLE(73, 1100100, 7) +HUFFMAN_TABLE(74, 1100101, 7) +HUFFMAN_TABLE(75, 1100110, 7) +HUFFMAN_TABLE(76, 1100111, 7) +HUFFMAN_TABLE(77, 1101000, 7) +HUFFMAN_TABLE(78, 1101001, 7) +HUFFMAN_TABLE(79, 1101010, 7) +HUFFMAN_TABLE(80, 1101011, 7) +HUFFMAN_TABLE(81, 1101100, 7) +HUFFMAN_TABLE(82, 1101101, 7) +HUFFMAN_TABLE(83, 1101110, 7) +HUFFMAN_TABLE(84, 1101111, 7) +HUFFMAN_TABLE(85, 1110000, 7) +HUFFMAN_TABLE(86, 1110001, 7) +HUFFMAN_TABLE(87, 1110010, 7) +HUFFMAN_TABLE(88, 11111100, 8) +HUFFMAN_TABLE(89, 1110011, 7) +HUFFMAN_TABLE(90, 11111101, 8) +HUFFMAN_TABLE(91, 1111111111011, 13) +HUFFMAN_TABLE(92, 1111111111111110000, 19) +HUFFMAN_TABLE(93, 1111111111100, 13) +HUFFMAN_TABLE(94, 11111111111100, 14) +HUFFMAN_TABLE(95, 100010, 6) +HUFFMAN_TABLE(96, 111111111111101, 15) +HUFFMAN_TABLE(97, 00011, 5) +HUFFMAN_TABLE(98, 100011, 6) +HUFFMAN_TABLE(99, 00100, 5) +HUFFMAN_TABLE(100, 100100, 6) +HUFFMAN_TABLE(101, 00101, 5) +HUFFMAN_TABLE(102, 100101, 6) +HUFFMAN_TABLE(103, 100110, 6) +HUFFMAN_TABLE(104, 100111, 6) +HUFFMAN_TABLE(105, 00110, 5) +HUFFMAN_TABLE(106, 1110100, 7) +HUFFMAN_TABLE(107, 1110101, 7) +HUFFMAN_TABLE(108, 101000, 6) +HUFFMAN_TABLE(109, 101001, 6) +HUFFMAN_TABLE(110, 101010, 6) +HUFFMAN_TABLE(111, 00111, 5) +HUFFMAN_TABLE(112, 101011, 6) +HUFFMAN_TABLE(113, 1110110, 7) +HUFFMAN_TABLE(114, 101100, 6) +HUFFMAN_TABLE(115, 01000, 5) +HUFFMAN_TABLE(116, 01001, 5) +HUFFMAN_TABLE(117, 101101, 6) +HUFFMAN_TABLE(118, 1110111, 7) +HUFFMAN_TABLE(119, 1111000, 7) +HUFFMAN_TABLE(120, 1111001, 7) +HUFFMAN_TABLE(121, 1111010, 7) +HUFFMAN_TABLE(122, 1111011, 7) +HUFFMAN_TABLE(123, 111111111111110, 15) +HUFFMAN_TABLE(124, 11111111100, 11) +HUFFMAN_TABLE(125, 11111111111101, 14) +HUFFMAN_TABLE(126, 1111111111101, 13) +HUFFMAN_TABLE(127, 1111111111111111111111111100, 28) +HUFFMAN_TABLE(128, 11111111111111100110, 20) +HUFFMAN_TABLE(129, 1111111111111111010010, 22) +HUFFMAN_TABLE(130, 11111111111111100111, 20) +HUFFMAN_TABLE(131, 11111111111111101000, 20) +HUFFMAN_TABLE(132, 1111111111111111010011, 22) +HUFFMAN_TABLE(133, 1111111111111111010100, 22) +HUFFMAN_TABLE(134, 1111111111111111010101, 22) +HUFFMAN_TABLE(135, 11111111111111111011001, 23) +HUFFMAN_TABLE(136, 1111111111111111010110, 22) +HUFFMAN_TABLE(137, 11111111111111111011010, 23) +HUFFMAN_TABLE(138, 11111111111111111011011, 23) +HUFFMAN_TABLE(139, 11111111111111111011100, 23) +HUFFMAN_TABLE(140, 11111111111111111011101, 23) +HUFFMAN_TABLE(141, 11111111111111111011110, 23) +HUFFMAN_TABLE(142, 111111111111111111101011, 24) +HUFFMAN_TABLE(143, 11111111111111111011111, 23) +HUFFMAN_TABLE(144, 111111111111111111101100, 24) +HUFFMAN_TABLE(145, 111111111111111111101101, 24) +HUFFMAN_TABLE(146, 1111111111111111010111, 22) +HUFFMAN_TABLE(147, 11111111111111111100000, 23) +HUFFMAN_TABLE(148, 111111111111111111101110, 24) +HUFFMAN_TABLE(149, 11111111111111111100001, 23) +HUFFMAN_TABLE(150, 11111111111111111100010, 23) +HUFFMAN_TABLE(151, 11111111111111111100011, 23) +HUFFMAN_TABLE(152, 11111111111111111100100, 23) +HUFFMAN_TABLE(153, 111111111111111011100, 21) +HUFFMAN_TABLE(154, 1111111111111111011000, 22) +HUFFMAN_TABLE(155, 11111111111111111100101, 23) +HUFFMAN_TABLE(156, 1111111111111111011001, 22) +HUFFMAN_TABLE(157, 11111111111111111100110, 23) +HUFFMAN_TABLE(158, 11111111111111111100111, 23) +HUFFMAN_TABLE(159, 111111111111111111101111, 24) +HUFFMAN_TABLE(160, 1111111111111111011010, 22) +HUFFMAN_TABLE(161, 111111111111111011101, 21) +HUFFMAN_TABLE(162, 11111111111111101001, 20) +HUFFMAN_TABLE(163, 1111111111111111011011, 22) +HUFFMAN_TABLE(164, 1111111111111111011100, 22) +HUFFMAN_TABLE(165, 11111111111111111101000, 23) +HUFFMAN_TABLE(166, 11111111111111111101001, 23) +HUFFMAN_TABLE(167, 111111111111111011110, 21) +HUFFMAN_TABLE(168, 11111111111111111101010, 23) +HUFFMAN_TABLE(169, 1111111111111111011101, 22) +HUFFMAN_TABLE(170, 1111111111111111011110, 22) +HUFFMAN_TABLE(171, 111111111111111111110000, 24) +HUFFMAN_TABLE(172, 111111111111111011111, 21) +HUFFMAN_TABLE(173, 1111111111111111011111, 22) +HUFFMAN_TABLE(174, 11111111111111111101011, 23) +HUFFMAN_TABLE(175, 11111111111111111101100, 23) +HUFFMAN_TABLE(176, 111111111111111100000, 21) +HUFFMAN_TABLE(177, 111111111111111100001, 21) +HUFFMAN_TABLE(178, 1111111111111111100000, 22) +HUFFMAN_TABLE(179, 111111111111111100010, 21) +HUFFMAN_TABLE(180, 11111111111111111101101, 23) +HUFFMAN_TABLE(181, 1111111111111111100001, 22) +HUFFMAN_TABLE(182, 11111111111111111101110, 23) +HUFFMAN_TABLE(183, 11111111111111111101111, 23) +HUFFMAN_TABLE(184, 11111111111111101010, 20) +HUFFMAN_TABLE(185, 1111111111111111100010, 22) +HUFFMAN_TABLE(186, 1111111111111111100011, 22) +HUFFMAN_TABLE(187, 1111111111111111100100, 22) +HUFFMAN_TABLE(188, 11111111111111111110000, 23) +HUFFMAN_TABLE(189, 1111111111111111100101, 22) +HUFFMAN_TABLE(190, 1111111111111111100110, 22) +HUFFMAN_TABLE(191, 11111111111111111110001, 23) +HUFFMAN_TABLE(192, 11111111111111111111100000, 26) +HUFFMAN_TABLE(193, 11111111111111111111100001, 26) +HUFFMAN_TABLE(194, 11111111111111101011, 20) +HUFFMAN_TABLE(195, 1111111111111110001, 19) +HUFFMAN_TABLE(196, 1111111111111111100111, 22) +HUFFMAN_TABLE(197, 11111111111111111110010, 23) +HUFFMAN_TABLE(198, 1111111111111111101000, 22) +HUFFMAN_TABLE(199, 1111111111111111111101100, 25) +HUFFMAN_TABLE(200, 11111111111111111111100010, 26) +HUFFMAN_TABLE(201, 11111111111111111111100011, 26) +HUFFMAN_TABLE(202, 11111111111111111111100100, 26) +HUFFMAN_TABLE(203, 111111111111111111111011110, 27) +HUFFMAN_TABLE(204, 111111111111111111111011111, 27) +HUFFMAN_TABLE(205, 11111111111111111111100101, 26) +HUFFMAN_TABLE(206, 111111111111111111110001, 24) +HUFFMAN_TABLE(207, 1111111111111111111101101, 25) +HUFFMAN_TABLE(208, 1111111111111110010, 19) +HUFFMAN_TABLE(209, 111111111111111100011, 21) +HUFFMAN_TABLE(210, 11111111111111111111100110, 26) +HUFFMAN_TABLE(211, 111111111111111111111100000, 27) +HUFFMAN_TABLE(212, 111111111111111111111100001, 27) +HUFFMAN_TABLE(213, 11111111111111111111100111, 26) +HUFFMAN_TABLE(214, 111111111111111111111100010, 27) +HUFFMAN_TABLE(215, 111111111111111111110010, 24) +HUFFMAN_TABLE(216, 111111111111111100100, 21) +HUFFMAN_TABLE(217, 111111111111111100101, 21) +HUFFMAN_TABLE(218, 11111111111111111111101000, 26) +HUFFMAN_TABLE(219, 11111111111111111111101001, 26) +HUFFMAN_TABLE(220, 1111111111111111111111111101, 28) +HUFFMAN_TABLE(221, 111111111111111111111100011, 27) +HUFFMAN_TABLE(222, 111111111111111111111100100, 27) +HUFFMAN_TABLE(223, 111111111111111111111100101, 27) +HUFFMAN_TABLE(224, 11111111111111101100, 20) +HUFFMAN_TABLE(225, 111111111111111111110011, 24) +HUFFMAN_TABLE(226, 11111111111111101101, 20) +HUFFMAN_TABLE(227, 111111111111111100110, 21) +HUFFMAN_TABLE(228, 1111111111111111101001, 22) +HUFFMAN_TABLE(229, 111111111111111100111, 21) +HUFFMAN_TABLE(230, 111111111111111101000, 21) +HUFFMAN_TABLE(231, 11111111111111111110011, 23) +HUFFMAN_TABLE(232, 1111111111111111101010, 22) +HUFFMAN_TABLE(233, 1111111111111111101011, 22) +HUFFMAN_TABLE(234, 1111111111111111111101110, 25) +HUFFMAN_TABLE(235, 1111111111111111111101111, 25) +HUFFMAN_TABLE(236, 111111111111111111110100, 24) +HUFFMAN_TABLE(237, 111111111111111111110101, 24) +HUFFMAN_TABLE(238, 11111111111111111111101010, 26) +HUFFMAN_TABLE(239, 11111111111111111110100, 23) +HUFFMAN_TABLE(240, 11111111111111111111101011, 26) +HUFFMAN_TABLE(241, 111111111111111111111100110, 27) +HUFFMAN_TABLE(242, 11111111111111111111101100, 26) +HUFFMAN_TABLE(243, 11111111111111111111101101, 26) +HUFFMAN_TABLE(244, 111111111111111111111100111, 27) +HUFFMAN_TABLE(245, 111111111111111111111101000, 27) +HUFFMAN_TABLE(246, 111111111111111111111101001, 27) +HUFFMAN_TABLE(247, 111111111111111111111101010, 27) +HUFFMAN_TABLE(248, 111111111111111111111101011, 27) +HUFFMAN_TABLE(249, 1111111111111111111111111110, 28) +HUFFMAN_TABLE(250, 111111111111111111111101100, 27) +HUFFMAN_TABLE(251, 111111111111111111111101101, 27) +HUFFMAN_TABLE(252, 111111111111111111111101110, 27) +HUFFMAN_TABLE(253, 111111111111111111111101111, 27) +HUFFMAN_TABLE(254, 111111111111111111111110000, 27) +HUFFMAN_TABLE(255, 11111111111111111111101110, 26) +HUFFMAN_TABLE(256, 111111111111111111111111111111, 30) + +#undef HUFFMAN_TABLE diff --git a/include/hpack/integers.hpp b/include/hpack/integers.hpp new file mode 100644 index 0000000..38fdf9f --- /dev/null +++ b/include/hpack/integers.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include "hpack/basic_types.hpp" + +namespace hpack { + +// postcondition: do not overwrites highest 8 - N bits in *out first bytee +// precondition: low N bits of *out first byte is 0 +template +O encode_integer(std::type_identity_t I, uint8_t N, O _out) noexcept { + auto out = noexport::adapt_output_iterator(_out); + assert(N <= 8 && ((*out & ((1 << N) - 1)) == 0)); + /* + pseudocode from RFC + if I < 2^N - 1, encode I on N bits + else + encode (2^N - 1) on N bits + I = I - (2^N - 1) + while I >= 128 + encode (I % 128 + 128) on 8 bits + I = I / 128 + encode I on 8 bits + */ + const uint8_t prefix_max = (1 << N) - 1; + assert((*out & prefix_max) == 0 && "precondition: low N bits of *out first byte is 0"); + auto push = [&out](uint8_t c) { + *out = c; + ++out; + }; + if (I < prefix_max) { + // write byte without overwriting existing first 8 - N bits + *out |= uint8_t(I); + ++out; + return noexport::unadapt(out); + } + // write byte without overwriting existing first 8 - N bits + *out |= prefix_max; + ++out; + I -= prefix_max; + while (I >= 128) { + auto quot = I / 128; + auto rem = I % 128; + I = quot; + push(rem | 0b1000'0000); + } + push(I); + return noexport::unadapt(out); +} + +template +[[nodiscard]] size_type decode_integer(In& in, In e, uint8_t N) { + const UInt prefix_mask = (1 << N) - 1; + // get first N bits + auto pull = [&] { + if (in == e) + handle_size_error(); + int8_t i = *in; + ++in; + return i; + }; + UInt I = pull() & prefix_mask; + if (I < prefix_mask) + return I; + uint8_t M = 0; + uint8_t B; + do { + B = pull(); + UInt cpy = I; + I += UInt(B & 0b0111'1111) << M; + if (I < cpy) // overflow + handle_protocol_error(); + M += 7; + } while (B & 0b1000'0000); + return I; +} + +} // namespace hpack diff --git a/include/hpack/static_table.def b/include/hpack/static_table.def new file mode 100644 index 0000000..6b625ad --- /dev/null +++ b/include/hpack/static_table.def @@ -0,0 +1,68 @@ + +#ifndef STATIC_TABLE_ENTRY +#error STATIC_TABLE_ENTRY(cppname, header_name, ... optional value) +#endif + +STATIC_TABLE_ENTRY(authority, ":authority") +STATIC_TABLE_ENTRY(method_get, ":method", "GET") +STATIC_TABLE_ENTRY(method_post, ":method", "POST") +STATIC_TABLE_ENTRY(path, ":path", "/") +STATIC_TABLE_ENTRY(path_index_html, ":path", "/index.html") +STATIC_TABLE_ENTRY(scheme_http, ":scheme", "http") +STATIC_TABLE_ENTRY(scheme_https, ":scheme", "https") +STATIC_TABLE_ENTRY(status_200, ":status", "200") +STATIC_TABLE_ENTRY(status_204, ":status", "204") +STATIC_TABLE_ENTRY(status_206, ":status", "206") +STATIC_TABLE_ENTRY(status_304, ":status", "304") +STATIC_TABLE_ENTRY(status_400, ":status", "400") +STATIC_TABLE_ENTRY(status_404, ":status", "404") +STATIC_TABLE_ENTRY(status_500, ":status", "500") +STATIC_TABLE_ENTRY(accept_charset, "accept-charset") +STATIC_TABLE_ENTRY(accept_encoding, "accept-encoding", "gzip, deflate") +STATIC_TABLE_ENTRY(accept_language, "accept-language") +STATIC_TABLE_ENTRY(accept_ranges, "accept-ranges") +STATIC_TABLE_ENTRY(accept, "accept") +STATIC_TABLE_ENTRY(access_control_allow_origin, "access-control-allow-origin") +STATIC_TABLE_ENTRY(age, "age") +STATIC_TABLE_ENTRY(allow, "allow") +STATIC_TABLE_ENTRY(authorization, "authorization") +STATIC_TABLE_ENTRY(cache_control, "cache-control") +STATIC_TABLE_ENTRY(content_disposition, "content-disposition") +STATIC_TABLE_ENTRY(content_encoding, "content-encoding") +STATIC_TABLE_ENTRY(content_language, "content-language") +STATIC_TABLE_ENTRY(content_length, "content-length") +STATIC_TABLE_ENTRY(content_location, "content-location") +STATIC_TABLE_ENTRY(content_range, "content-range") +STATIC_TABLE_ENTRY(content_type, "content-type") +STATIC_TABLE_ENTRY(cookie, "cookie") +STATIC_TABLE_ENTRY(date, "date") +STATIC_TABLE_ENTRY(etag, "etag") +STATIC_TABLE_ENTRY(expect, "expect") +STATIC_TABLE_ENTRY(expires, "expires") +STATIC_TABLE_ENTRY(from, "from") +STATIC_TABLE_ENTRY(host, "host") +STATIC_TABLE_ENTRY(if_match, "if-match") +STATIC_TABLE_ENTRY(if_modified_since, "if-modified-since") +STATIC_TABLE_ENTRY(if_none_match, "if-none-match") +STATIC_TABLE_ENTRY(if_range, "if-range") +STATIC_TABLE_ENTRY(if_unmodified_since, "if-unmodified-since") +STATIC_TABLE_ENTRY(last_modified, "last-modified") +STATIC_TABLE_ENTRY(link, "link") +STATIC_TABLE_ENTRY(location, "location") +STATIC_TABLE_ENTRY(max_forwards, "max-forwards") +STATIC_TABLE_ENTRY(proxy_authenticate, "proxy-authenticate") +STATIC_TABLE_ENTRY(proxy_authorization, "proxy-authorization") +STATIC_TABLE_ENTRY(range, "range") +STATIC_TABLE_ENTRY(referer, "referer") +STATIC_TABLE_ENTRY(refresh, "refresh") +STATIC_TABLE_ENTRY(retry_after, "retry-after") +STATIC_TABLE_ENTRY(server, "server") +STATIC_TABLE_ENTRY(set_cookie, "set-cookie") +STATIC_TABLE_ENTRY(strict_transport_security , "strict-transport-security") +STATIC_TABLE_ENTRY(transfer_encoding, "transfer-encoding") +STATIC_TABLE_ENTRY(user_agent, "user-agent") +STATIC_TABLE_ENTRY(vary, "vary") +STATIC_TABLE_ENTRY(via, "via") +STATIC_TABLE_ENTRY(www_authenticate, "www-authenticate") + +#undef STATIC_TABLE_ENTRY diff --git a/include/hpack/static_table.hpp b/include/hpack/static_table.hpp new file mode 100644 index 0000000..b1b0a79 --- /dev/null +++ b/include/hpack/static_table.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "hpack/basic_types.hpp" + +namespace hpack { + +struct static_table_t { + enum values : uint8_t { + not_found = 0, +#define STATIC_TABLE_ENTRY(cppname, ...) cppname, +#include "hpack/static_table.def" + first_unused_index, + }; + // postcondition: returns < first_unused_index() + // and 0 ('not_found') when not found + static index_type find(std::string_view name) noexcept; + + static find_result_t find(std::string_view name, std::string_view value) noexcept; + + // returns 'not_found' if not found + static index_type find_by_value(std::string_view value) noexcept; + + [[nodiscard]] static find_result_t find(index_type name, std::string_view value) noexcept; + + // precondition: index < first_unused_index && index != 0 + // .value empty if no cached + static table_entry get_entry(index_type index); +}; + +} // namespace hpack diff --git a/include/hpack/strings.hpp b/include/hpack/strings.hpp new file mode 100644 index 0000000..a12e4bc --- /dev/null +++ b/include/hpack/strings.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include "hpack/basic_types.hpp" +#include "hpack/integers.hpp" + +namespace hpack { + +template +O encode_string_huffman(std::string_view str, O _out) { + auto out = noexport::adapt_output_iterator(_out); + // precalculate size + // (size should be before string and len in bits depends on 'len' value) + size_type len_after_encode = 0; + for (char c : str) + len_after_encode += huffman_table[uint8_t(c)].bit_count; + *out = 0b1000'0000; // set H bit + const int padlen = 8 - len_after_encode % 8; + out = encode_integer((len_after_encode + padlen) / 8, 7, out); + auto push_bit = [&, bitn = 7](bool bit) mutable { + if (bitn == 7) + *out = 0; + *out |= (bit << bitn); + if (bitn == 0) { + ++out; + bitn = 7; + // not set out to 0, because may be end + } else { + --bitn; + } + }; + for (char c : str) { + sym_info_t bits = huffman_table[uint8_t(c)]; + for (int i = 0; i < bits.bit_count; ++i) + push_bit(bits.bits & (1 << i)); + } + // padding MUST BE formed from EOS la-la-la (just 111..) + for (int i = 0; i < padlen; ++i) + push_bit(true); + return noexport::unadapt(out); +} + +template +O encode_string(std::string_view str, O _out) { + auto out = noexport::adapt_output_iterator(_out); + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | H | String Length (7+) | + +---+---------------------------+ + | String Data (Length octets) | + +-------------------------------+ + */ + if constexpr (!Huffman) { + *out = 0; // set H bit to 0 + out = encode_integer(str.size(), 7, out); + out = std::copy_n(str.data(), str.size(), out); + } else { + out = encode_string_huffman(str, out); + } + return noexport::unadapt(out); +} + +// in decoder.hpp +struct decoded_string; + +void decode_string(In& in, In e, decoded_string& out); + +} // namespace hpack diff --git a/src/decoder.cpp b/src/decoder.cpp new file mode 100644 index 0000000..f70d82e --- /dev/null +++ b/src/decoder.cpp @@ -0,0 +1,223 @@ + +#include + +#include "hpack/integers.hpp" +#include "hpack/decoder.hpp" + +namespace { + +template +struct scope_fail { + F foo; + bool failed = true; + + ~scope_fail() { + if (failed) + foo(); + } +}; +template +scope_fail(T) -> scope_fail; + +} // namespace + +namespace hpack { + +[[nodiscard]] constexpr static size_t max_huffman_string_size_after_decode( + size_type huffman_str_len) noexcept { + // minimal symbol in table is 5 bit len, so worst case is only 5 bit symbols + return size_t(huffman_str_len) * 8 / 5; +} + +// uint16_t(-1) if not found +uint16_t huffman_decode_table_find(sym_info_t info); + +// precondition: in != e +// note: 'len' must be decoded before calling this function +template +static O decode_string_huffman(In in, size_type len, O out) { + In e = in + len; + sym_info_t info{0, 0}; + int bit_nmb = 0; + auto next_bit = [&] { + bool bit = *in & (0b1000'0000 >> bit_nmb); + if (bit_nmb == 7) { + bit_nmb = 0; + ++in; + } else { + ++bit_nmb; + } + return bit; + }; + for (;;) { + // min symbol len in Huffman table is 5 bits + for (int i = 0; in != e && i < 5; ++i, ++info.bit_count) { + info.bits <<= 1; + info.bits += next_bit(); + } + uint16_t sym; + while ((sym = huffman_decode_table_find(info)) == uint16_t(-1) && in != e) { + info.bits <<= 1; + info.bits += next_bit(); + ++info.bit_count; + } + if (sym == 256) [[unlikely]] { + // EOS + while (bit_nmb != 0) // skip padding + next_bit(); + return out; + } + if (sym != uint16_t(-1)) { + info = {}; + *out = byte_t(sym); + ++out; + } + if (in == e) { + if (std::countr_one(info.bits) != info.bit_count) + handle_protocol_error(); // incorrect padding + return out; + } + } + return out; +} + +void decoded_string::set_huffman(const char* ptr, size_type len) { + // also handles case when len == 0 + if (bytes_allocated() >= max_huffman_string_size_after_decode(len)) { + const byte_t* in = (const byte_t*)ptr; + // const cast because im owner of pointer (its allocated by malloc) + char* end = decode_string_huffman(in, len, const_cast(data)); + sz = end - data; + + assert(sz <= max_huffman_string_size_after_decode(sz)); + } else { + size_t sz_to_allocate = std::bit_ceil(max_huffman_string_size_after_decode(len)); + allocated_sz_log2 = std::bit_width(sz_to_allocate) - 1; + const char* old_data = data; + data = (char*)malloc(sz_to_allocate); + + scope_fail free_mem{[&] { + free((void*)data); + data = old_data; + allocated_sz_log2 = 0; + }}; + // recursive call into branch where we have enough memory + set_huffman(ptr, len); + + free_mem.failed = false; + } +} + +// decodes partly indexed / new-name pairs +static void decode_header_impl(In& in, In e, uint8_t N, dynamic_table_t& dyntab, header_view& out) { + index_type index = decode_integer(in, e, N); + if (index == 0) + decode_string(in, e, out.name); + else + out.name = get_by_index(index, &dyntab).name; + decode_string(in, e, out.value); +} + +static void decode_header_fully_indexed(In& in, In e, dynamic_table_t& dyntab, header_view& out) { + assert(*in & 0b1000'0000); + index_type index = decode_integer(in, e, 7); + table_entry entry = get_by_index(index, &dyntab); + // only way to get uncached value is from static table, + // in dynamic table empty header value ("") is a cached header + if (index < static_table_t::first_unused_index && entry.value.empty()) + handle_protocol_error(); + out = entry; +} + +// header with incremental indexing +static void decode_header_cache(In& in, In e, dynamic_table_t& dyntab, header_view& out) { + assert(in != e && *in & 0b0100'0000); + decode_header_impl(in, e, 6, dyntab, out); + dyntab.add_entry(out.name.str(), out.value.str()); +} + +static void decode_header_without_indexing(In& in, In e, dynamic_table_t& dyntab, header_view& out) { + assert(in != e && (*in & 0x1111'0000) == 0); + return decode_header_impl(in, e, 4, dyntab, out); +} + +static void decode_header_never_indexing(In& in, In e, dynamic_table_t& dyntab, header_view& out) { + assert(in != e && *in & 0b0001'0000); + return decode_header_impl(in, e, 4, dyntab, out); +} + +// returns requested new size of dynamic table +static size_type decode_dynamic_table_size_update(In& in, In e) noexcept { + assert(*in & 0b0010'0000 && !(*in & 0b0100'0000) && !(*in & 0b1000'0000)); + return decode_integer(in, e, 5); +} + +void decode_string(In& in, In e, decoded_string& out) { + bool is_huffman = *in & 0b1000'0000; + size_type str_len = decode_integer(in, e, 7); + if (str_len > std::distance(in, e)) + handle_size_error(); + if (is_huffman) + out.set_huffman((const char*)in, str_len); + else + out = std::string_view((const char*)in, str_len); + in += str_len; +} + +void decoder::decode_header(In& in, In e, header_view& out) { + if (*in & 0b1000'0000) + return decode_header_fully_indexed(in, e, dyntab, out); + if (*in & 0b0100'0000) + return decode_header_cache(in, e, dyntab, out); + if (*in & 0b0010'0000) { + dyntab.update_size(decode_dynamic_table_size_update(in, e)); + out.name.reset(); + out.value.reset(); + return; + } + if (*in & 0b0001'0000) + return decode_header_never_indexing(in, e, dyntab, out); + if ((*in & 0b1111'0000) == 0) + return decode_header_without_indexing(in, e, dyntab, out); + handle_protocol_error(); +} + +// returns status code +int decoder::decode_response_status(In& in, In e) { + if (*in & 0b1000'0000) { + // fast path, fully indexed + auto in_before = in; + index_type index = decode_integer(in, e, 7); + switch (index) { + case static_table_t::status_200: + return 200; + case static_table_t::status_204: + return 204; + case static_table_t::status_206: + return 206; + case static_table_t::status_304: + return 304; + case static_table_t::status_400: + return 400; + case static_table_t::status_404: + return 404; + case static_table_t::status_500: + return 500; + } + in = in_before; + } + // first header of response must be required pseudoheader, + // which is (for response) only one - ":status" + header_view header; + decode_header(in, e, header); + std::string_view code = header.value.str(); + if (header.name.str() != ":status" || code.size() != 3) + handle_protocol_error(); + int status_code; + auto [_, err] = std::from_chars(code.data(), code.data() + 3, status_code); + if (err != std::errc{}) + handle_protocol_error(); + return status_code; +} + +} // namespace hpack diff --git a/src/dynamic_table.cpp b/src/dynamic_table.cpp new file mode 100644 index 0000000..1db02a2 --- /dev/null +++ b/src/dynamic_table.cpp @@ -0,0 +1,179 @@ + +#include "hpack/dynamic_table.hpp" + +#include +#include // memcpy + +namespace bi = boost::intrusive; + +namespace hpack { + +struct dynamic_table_t::entry_t : bi::set_base_hook> { + const size_type name_end; + const size_type value_end; + const size_t _insert_c; + char data[]; + + entry_t(size_type name_len, size_type value_len, size_t insert_c) noexcept + : name_end(name_len), value_end(name_len + value_len), _insert_c(insert_c) { + } + + std::string_view name() const noexcept { + return {data, data + name_end}; + } + std::string_view value() const noexcept { + return {data + name_end, data + value_end}; + } + size_type size() const noexcept { + return value_end; + } + + static entry_t* create(std::string_view name, std::string_view value, size_t insert_c, + std::pmr::memory_resource* resource) { + assert(resource); + void* bytes = resource->allocate(sizeof(entry_t) + name.size() + value.size(), alignof(entry_t)); + entry_t* e = new (bytes) entry_t(name.size(), value.size(), insert_c); + memcpy(+e->data, name.data(), name.size()); + memcpy(e->data + name.size(), value.data(), value.size()); + return e; + } + static void destroy(const entry_t* e, std::pmr::memory_resource* resource) noexcept { + assert(e && resource); + std::destroy_at(e); + resource->deallocate((void*)e, e->value_end, alignof(entry_t)); + } +}; + +// precondition: 'e' now in entries +index_type dynamic_table_t::indexof(const dynamic_table_t::entry_t& e) const noexcept { + return static_table_t::first_unused_index + (_insert_count - e._insert_c); +} + +static size_type entry_size(const dynamic_table_t::entry_t& entry) noexcept { + /* + The size of an entry is the sum of its name's length in octets (as + defined in Section 5.2), its value's length in octets, and 32. + + entry.name().size() + entry.value().size() + 32; + */ + return entry.value_end + 32; +} + +table_entry dynamic_table_t::key_of_entry::operator()(const dynamic_table_t::entry_t& v) const noexcept { + return {v.name(), v.value()}; +} + +dynamic_table_t::dynamic_table_t(size_type max_size, std::pmr::memory_resource* m) noexcept + : _current_size(0), + _max_size(max_size), + _insert_count(0), + _resource(m ? m : std::pmr::get_default_resource()) { +} + +dynamic_table_t::dynamic_table_t(dynamic_table_t&& other) noexcept + : entries(std::move(other.entries)), + set(std::move(other.set)), + _current_size(std::exchange(other._current_size, 0)), + _max_size(std::exchange(other._max_size, 0)), + _insert_count(std::exchange(other._insert_count, 0)), + _resource(std::exchange(other._resource, std::pmr::get_default_resource())) { +} + +dynamic_table_t& dynamic_table_t::operator=(dynamic_table_t&& other) noexcept { + if (this == &other) [[unlikely]] + return *this; + reset(); + entries = std::move(other.entries); + set = std::move(other.set); + _current_size = std::exchange(other._current_size, 0); + _max_size = std::exchange(other._max_size, 0); + _insert_count = std::exchange(other._insert_count, 0); + _resource = std::exchange(other._resource, std::pmr::get_default_resource()); + return *this; +} + +dynamic_table_t::~dynamic_table_t() { + reset(); +} + +// returns index of added pair, 0 if cannot add +index_type dynamic_table_t::add_entry(std::string_view name, std::string_view value) { + size_type new_entry_size = name.size() + value.size() + 32; + if (_max_size < new_entry_size) [[unlikely]] { + reset(); + return 0; + } + evict_until_fits_into(_max_size - new_entry_size); + entries.push_back(entry_t::create(name, value, ++_insert_count, _resource)); + set.insert(*entries.back()); + _current_size += new_entry_size; + return static_table_t::first_unused_index; +} + +void dynamic_table_t::update_size(size_type new_max_size) { + if (new_max_size > max_size()) + throw protocol_error{}; + evict_until_fits_into(new_max_size); + _max_size = new_max_size; +} + +find_result_t dynamic_table_t::find(std::string_view name, std::string_view value) noexcept { + find_result_t r; + auto it = set.find(table_entry(name, value)); + if (it == set.end()) + return r; + if (name == it->name()) { + r.header_name_index = indexof(*it); + if (value == it->value()) + r.value_indexed = true; + } + return r; +} +find_result_t dynamic_table_t::find(index_type name, std::string_view value) noexcept { + assert(name <= current_max_index()); + find_result_t r; + if (name < static_table_t::first_unused_index || name > current_max_index() || name == 0) + return r; + table_entry e = get_entry(name); + if (e.value == value) { + r.header_name_index = name; + r.value_indexed = true; + return r; + } + auto it = set.find(table_entry(e.name, value)); + assert(it != set.end()); + if (e.name == it->name()) { + r.header_name_index = indexof(*it); + if (value == it->value()) + r.value_indexed = true; + } + return r; +} + +void dynamic_table_t::reset() noexcept { + set.clear(); + for (entry_t* e : entries) + entry_t::destroy(e, _resource); + entries.clear(); + _current_size = 0; +} + +void dynamic_table_t::evict_until_fits_into(size_type bytes) noexcept { + assert(bytes <= _max_size); + size_type i = 0; + for (; _current_size > bytes; ++i) { + _current_size -= entry_size(*entries[i]); + set.erase(set.s_iterator_to(*entries[i])); + entry_t::destroy(entries[i], _resource); + } + // evicts should be rare operation + entries.erase(entries.begin(), entries.begin() + i); +} + +table_entry dynamic_table_t::get_entry(index_type index) const noexcept { + assert(index >= static_table_t::first_unused_index && index <= current_max_index()); + auto& e = *(&entries.back() - (index - static_table_t::first_unused_index)); + return table_entry{e->name(), e->value()}; +} + +} // namespace hpack diff --git a/src/huffman.cpp b/src/huffman.cpp new file mode 100644 index 0000000..11b9355 --- /dev/null +++ b/src/huffman.cpp @@ -0,0 +1,398 @@ + +#include +#include + +#include "hpack/basic_types.hpp" + +namespace hpack { + +static consteval sym_info_t create_sym_info(std::string_view value, int bitcount) { + assert(value.size() == bitcount); + sym_info_t info{.bits = 0, .bit_count = static_cast(bitcount)}; + for (int i = 0; i < bitcount; ++i) { + uint32_t bit = value[i] == '1'; + if (value[i] != '1' && value[i] != '0') + throw 42; + info.bits |= (bit << i); + } + return info; +} + +constexpr sym_info_t huffman_table[257] = { +#define HUFFMAN_TABLE(index, bits, bitcount) create_sym_info(#bits, bitcount), +#include "hpack/huffman_table.def" +}; + +uint16_t huffman_decode_table_find(sym_info_t info) { +#define HUFFMAN_TABLE(index, bits, bitcount) \ + case bits: \ + return uint16_t(index); + switch (info.bit_count) { + case 5: { + switch (info.bits) { + HUFFMAN_TABLE(48, 0b00000, 5) + HUFFMAN_TABLE(49, 0b00001, 5) + HUFFMAN_TABLE(50, 0b00010, 5) + HUFFMAN_TABLE(97, 0b00011, 5) + HUFFMAN_TABLE(99, 0b00100, 5) + HUFFMAN_TABLE(101, 0b00101, 5) + HUFFMAN_TABLE(105, 0b00110, 5) + HUFFMAN_TABLE(111, 0b00111, 5) + HUFFMAN_TABLE(115, 0b01000, 5) + HUFFMAN_TABLE(116, 0b01001, 5) + } + goto end; + } + case 6: { + switch (info.bits) { + HUFFMAN_TABLE(32, 0b010100, 6) + HUFFMAN_TABLE(37, 0b010101, 6) + HUFFMAN_TABLE(45, 0b010110, 6) + HUFFMAN_TABLE(46, 0b010111, 6) + HUFFMAN_TABLE(47, 0b011000, 6) + HUFFMAN_TABLE(51, 0b011001, 6) + HUFFMAN_TABLE(52, 0b011010, 6) + HUFFMAN_TABLE(53, 0b011011, 6) + HUFFMAN_TABLE(54, 0b011100, 6) + HUFFMAN_TABLE(55, 0b011101, 6) + HUFFMAN_TABLE(56, 0b011110, 6) + HUFFMAN_TABLE(57, 0b011111, 6) + HUFFMAN_TABLE(61, 0b100000, 6) + HUFFMAN_TABLE(65, 0b100001, 6) + HUFFMAN_TABLE(95, 0b100010, 6) + HUFFMAN_TABLE(98, 0b100011, 6) + HUFFMAN_TABLE(100, 0b100100, 6) + HUFFMAN_TABLE(102, 0b100101, 6) + HUFFMAN_TABLE(103, 0b100110, 6) + HUFFMAN_TABLE(104, 0b100111, 6) + HUFFMAN_TABLE(108, 0b101000, 6) + HUFFMAN_TABLE(109, 0b101001, 6) + HUFFMAN_TABLE(110, 0b101010, 6) + HUFFMAN_TABLE(112, 0b101011, 6) + HUFFMAN_TABLE(114, 0b101100, 6) + HUFFMAN_TABLE(117, 0b101101, 6) + } + goto end; + } + case 7: { + switch (info.bits) { + HUFFMAN_TABLE(58, 0b1011100, 7) + HUFFMAN_TABLE(66, 0b1011101, 7) + HUFFMAN_TABLE(67, 0b1011110, 7) + HUFFMAN_TABLE(68, 0b1011111, 7) + HUFFMAN_TABLE(69, 0b1100000, 7) + HUFFMAN_TABLE(70, 0b1100001, 7) + HUFFMAN_TABLE(71, 0b1100010, 7) + HUFFMAN_TABLE(72, 0b1100011, 7) + HUFFMAN_TABLE(73, 0b1100100, 7) + HUFFMAN_TABLE(74, 0b1100101, 7) + HUFFMAN_TABLE(75, 0b1100110, 7) + HUFFMAN_TABLE(76, 0b1100111, 7) + HUFFMAN_TABLE(77, 0b1101000, 7) + HUFFMAN_TABLE(78, 0b1101001, 7) + HUFFMAN_TABLE(79, 0b1101010, 7) + HUFFMAN_TABLE(80, 0b1101011, 7) + HUFFMAN_TABLE(81, 0b1101100, 7) + HUFFMAN_TABLE(82, 0b1101101, 7) + HUFFMAN_TABLE(83, 0b1101110, 7) + HUFFMAN_TABLE(84, 0b1101111, 7) + HUFFMAN_TABLE(85, 0b1110000, 7) + HUFFMAN_TABLE(86, 0b1110001, 7) + HUFFMAN_TABLE(87, 0b1110010, 7) + HUFFMAN_TABLE(89, 0b1110011, 7) + HUFFMAN_TABLE(106, 0b1110100, 7) + HUFFMAN_TABLE(107, 0b1110101, 7) + HUFFMAN_TABLE(113, 0b1110110, 7) + HUFFMAN_TABLE(118, 0b1110111, 7) + HUFFMAN_TABLE(119, 0b1111000, 7) + HUFFMAN_TABLE(120, 0b1111001, 7) + HUFFMAN_TABLE(121, 0b1111010, 7) + HUFFMAN_TABLE(122, 0b1111011, 7) + } + goto end; + } + case 8: { + switch (info.bits) { + HUFFMAN_TABLE(38, 0b11111000, 8) + HUFFMAN_TABLE(42, 0b11111001, 8) + HUFFMAN_TABLE(44, 0b11111010, 8) + HUFFMAN_TABLE(59, 0b11111011, 8) + HUFFMAN_TABLE(88, 0b11111100, 8) + HUFFMAN_TABLE(90, 0b11111101, 8) + } + goto end; + } + case 10: { + switch (info.bits) { + HUFFMAN_TABLE(33, 0b1111111000, 10) + HUFFMAN_TABLE(34, 0b1111111001, 10) + HUFFMAN_TABLE(40, 0b1111111010, 10) + HUFFMAN_TABLE(41, 0b1111111011, 10) + HUFFMAN_TABLE(63, 0b1111111100, 10) + } + goto end; + } + case 11: { + switch (info.bits) { + HUFFMAN_TABLE(39, 0b11111111010, 11) + HUFFMAN_TABLE(43, 0b11111111011, 11) + HUFFMAN_TABLE(124, 0b11111111100, 11) + } + goto end; + } + case 12: { + switch (info.bits) { + HUFFMAN_TABLE(35, 0b111111111010, 12) + HUFFMAN_TABLE(62, 0b111111111011, 12) + } + goto end; + } + case 13: { + switch (info.bits) { + HUFFMAN_TABLE(0, 0b1111111111000, 13) + HUFFMAN_TABLE(36, 0b1111111111001, 13) + HUFFMAN_TABLE(64, 0b1111111111010, 13) + HUFFMAN_TABLE(91, 0b1111111111011, 13) + HUFFMAN_TABLE(93, 0b1111111111100, 13) + HUFFMAN_TABLE(126, 0b1111111111101, 13) + } + goto end; + } + case 14: { + switch (info.bits) { + HUFFMAN_TABLE(94, 0b11111111111100, 14) + HUFFMAN_TABLE(125, 0b11111111111101, 14) + } + goto end; + } + case 15: { + switch (info.bits) { + HUFFMAN_TABLE(60, 0b111111111111100, 15) + HUFFMAN_TABLE(96, 0b111111111111101, 15) + HUFFMAN_TABLE(123, 0b111111111111110, 15) + } + goto end; + } + case 19: { + switch (info.bits) { + HUFFMAN_TABLE(92, 0b1111111111111110000, 19) + HUFFMAN_TABLE(195, 0b1111111111111110001, 19) + HUFFMAN_TABLE(208, 0b1111111111111110010, 19) + } + goto end; + } + case 20: { + switch (info.bits) { + HUFFMAN_TABLE(128, 0b11111111111111100110, 20) + HUFFMAN_TABLE(130, 0b11111111111111100111, 20) + HUFFMAN_TABLE(131, 0b11111111111111101000, 20) + HUFFMAN_TABLE(162, 0b11111111111111101001, 20) + HUFFMAN_TABLE(184, 0b11111111111111101010, 20) + HUFFMAN_TABLE(194, 0b11111111111111101011, 20) + HUFFMAN_TABLE(224, 0b11111111111111101100, 20) + HUFFMAN_TABLE(226, 0b11111111111111101101, 20) + } + goto end; + } + case 21: { + switch (info.bits) { + HUFFMAN_TABLE(153, 0b111111111111111011100, 21) + HUFFMAN_TABLE(161, 0b111111111111111011101, 21) + HUFFMAN_TABLE(167, 0b111111111111111011110, 21) + HUFFMAN_TABLE(172, 0b111111111111111011111, 21) + HUFFMAN_TABLE(176, 0b111111111111111100000, 21) + HUFFMAN_TABLE(177, 0b111111111111111100001, 21) + HUFFMAN_TABLE(179, 0b111111111111111100010, 21) + HUFFMAN_TABLE(209, 0b111111111111111100011, 21) + HUFFMAN_TABLE(216, 0b111111111111111100100, 21) + HUFFMAN_TABLE(217, 0b111111111111111100101, 21) + HUFFMAN_TABLE(227, 0b111111111111111100110, 21) + HUFFMAN_TABLE(229, 0b111111111111111100111, 21) + HUFFMAN_TABLE(230, 0b111111111111111101000, 21) + } + goto end; + } + case 22: { + switch (info.bits) { + HUFFMAN_TABLE(129, 0b1111111111111111010010, 22) + HUFFMAN_TABLE(132, 0b1111111111111111010011, 22) + HUFFMAN_TABLE(133, 0b1111111111111111010100, 22) + HUFFMAN_TABLE(134, 0b1111111111111111010101, 22) + HUFFMAN_TABLE(136, 0b1111111111111111010110, 22) + HUFFMAN_TABLE(146, 0b1111111111111111010111, 22) + HUFFMAN_TABLE(154, 0b1111111111111111011000, 22) + HUFFMAN_TABLE(156, 0b1111111111111111011001, 22) + HUFFMAN_TABLE(160, 0b1111111111111111011010, 22) + HUFFMAN_TABLE(163, 0b1111111111111111011011, 22) + HUFFMAN_TABLE(164, 0b1111111111111111011100, 22) + HUFFMAN_TABLE(169, 0b1111111111111111011101, 22) + HUFFMAN_TABLE(170, 0b1111111111111111011110, 22) + HUFFMAN_TABLE(173, 0b1111111111111111011111, 22) + HUFFMAN_TABLE(178, 0b1111111111111111100000, 22) + HUFFMAN_TABLE(181, 0b1111111111111111100001, 22) + HUFFMAN_TABLE(185, 0b1111111111111111100010, 22) + HUFFMAN_TABLE(186, 0b1111111111111111100011, 22) + HUFFMAN_TABLE(187, 0b1111111111111111100100, 22) + HUFFMAN_TABLE(189, 0b1111111111111111100101, 22) + HUFFMAN_TABLE(190, 0b1111111111111111100110, 22) + HUFFMAN_TABLE(196, 0b1111111111111111100111, 22) + HUFFMAN_TABLE(198, 0b1111111111111111101000, 22) + HUFFMAN_TABLE(228, 0b1111111111111111101001, 22) + HUFFMAN_TABLE(232, 0b1111111111111111101010, 22) + HUFFMAN_TABLE(233, 0b1111111111111111101011, 22) + } + goto end; + } + case 23: { + switch (info.bits) { + HUFFMAN_TABLE(1, 0b11111111111111111011000, 23) + HUFFMAN_TABLE(135, 0b11111111111111111011001, 23) + HUFFMAN_TABLE(137, 0b11111111111111111011010, 23) + HUFFMAN_TABLE(138, 0b11111111111111111011011, 23) + HUFFMAN_TABLE(139, 0b11111111111111111011100, 23) + HUFFMAN_TABLE(140, 0b11111111111111111011101, 23) + HUFFMAN_TABLE(141, 0b11111111111111111011110, 23) + HUFFMAN_TABLE(143, 0b11111111111111111011111, 23) + HUFFMAN_TABLE(147, 0b11111111111111111100000, 23) + HUFFMAN_TABLE(149, 0b11111111111111111100001, 23) + HUFFMAN_TABLE(150, 0b11111111111111111100010, 23) + HUFFMAN_TABLE(151, 0b11111111111111111100011, 23) + HUFFMAN_TABLE(152, 0b11111111111111111100100, 23) + HUFFMAN_TABLE(155, 0b11111111111111111100101, 23) + HUFFMAN_TABLE(157, 0b11111111111111111100110, 23) + HUFFMAN_TABLE(158, 0b11111111111111111100111, 23) + HUFFMAN_TABLE(165, 0b11111111111111111101000, 23) + HUFFMAN_TABLE(166, 0b11111111111111111101001, 23) + HUFFMAN_TABLE(168, 0b11111111111111111101010, 23) + HUFFMAN_TABLE(174, 0b11111111111111111101011, 23) + HUFFMAN_TABLE(175, 0b11111111111111111101100, 23) + HUFFMAN_TABLE(180, 0b11111111111111111101101, 23) + HUFFMAN_TABLE(182, 0b11111111111111111101110, 23) + HUFFMAN_TABLE(183, 0b11111111111111111101111, 23) + HUFFMAN_TABLE(188, 0b11111111111111111110000, 23) + HUFFMAN_TABLE(191, 0b11111111111111111110001, 23) + HUFFMAN_TABLE(197, 0b11111111111111111110010, 23) + HUFFMAN_TABLE(231, 0b11111111111111111110011, 23) + HUFFMAN_TABLE(239, 0b11111111111111111110100, 23) + } + goto end; + } + case 24: { + switch (info.bits) { + HUFFMAN_TABLE(9, 0b111111111111111111101010, 24) + HUFFMAN_TABLE(142, 0b111111111111111111101011, 24) + HUFFMAN_TABLE(144, 0b111111111111111111101100, 24) + HUFFMAN_TABLE(145, 0b111111111111111111101101, 24) + HUFFMAN_TABLE(148, 0b111111111111111111101110, 24) + HUFFMAN_TABLE(159, 0b111111111111111111101111, 24) + HUFFMAN_TABLE(171, 0b111111111111111111110000, 24) + HUFFMAN_TABLE(206, 0b111111111111111111110001, 24) + HUFFMAN_TABLE(215, 0b111111111111111111110010, 24) + HUFFMAN_TABLE(225, 0b111111111111111111110011, 24) + HUFFMAN_TABLE(236, 0b111111111111111111110100, 24) + HUFFMAN_TABLE(237, 0b111111111111111111110101, 24) + } + goto end; + } + case 25: { + switch (info.bits) { + HUFFMAN_TABLE(199, 0b1111111111111111111101100, 25) + HUFFMAN_TABLE(207, 0b1111111111111111111101101, 25) + HUFFMAN_TABLE(234, 0b1111111111111111111101110, 25) + HUFFMAN_TABLE(235, 0b1111111111111111111101111, 25) + } + goto end; + } + case 26: { + switch (info.bits) { + HUFFMAN_TABLE(192, 0b11111111111111111111100000, 26) + HUFFMAN_TABLE(193, 0b11111111111111111111100001, 26) + HUFFMAN_TABLE(200, 0b11111111111111111111100010, 26) + HUFFMAN_TABLE(201, 0b11111111111111111111100011, 26) + HUFFMAN_TABLE(202, 0b11111111111111111111100100, 26) + HUFFMAN_TABLE(205, 0b11111111111111111111100101, 26) + HUFFMAN_TABLE(210, 0b11111111111111111111100110, 26) + HUFFMAN_TABLE(213, 0b11111111111111111111100111, 26) + HUFFMAN_TABLE(218, 0b11111111111111111111101000, 26) + HUFFMAN_TABLE(219, 0b11111111111111111111101001, 26) + HUFFMAN_TABLE(238, 0b11111111111111111111101010, 26) + HUFFMAN_TABLE(240, 0b11111111111111111111101011, 26) + HUFFMAN_TABLE(242, 0b11111111111111111111101100, 26) + HUFFMAN_TABLE(243, 0b11111111111111111111101101, 26) + HUFFMAN_TABLE(255, 0b11111111111111111111101110, 26) + } + goto end; + } + case 27: { + switch (info.bits) { + HUFFMAN_TABLE(203, 0b111111111111111111111011110, 27) + HUFFMAN_TABLE(204, 0b111111111111111111111011111, 27) + HUFFMAN_TABLE(211, 0b111111111111111111111100000, 27) + HUFFMAN_TABLE(212, 0b111111111111111111111100001, 27) + HUFFMAN_TABLE(214, 0b111111111111111111111100010, 27) + HUFFMAN_TABLE(221, 0b111111111111111111111100011, 27) + HUFFMAN_TABLE(222, 0b111111111111111111111100100, 27) + HUFFMAN_TABLE(223, 0b111111111111111111111100101, 27) + HUFFMAN_TABLE(241, 0b111111111111111111111100110, 27) + HUFFMAN_TABLE(244, 0b111111111111111111111100111, 27) + HUFFMAN_TABLE(245, 0b111111111111111111111101000, 27) + HUFFMAN_TABLE(246, 0b111111111111111111111101001, 27) + HUFFMAN_TABLE(247, 0b111111111111111111111101010, 27) + HUFFMAN_TABLE(248, 0b111111111111111111111101011, 27) + HUFFMAN_TABLE(250, 0b111111111111111111111101100, 27) + HUFFMAN_TABLE(251, 0b111111111111111111111101101, 27) + HUFFMAN_TABLE(252, 0b111111111111111111111101110, 27) + HUFFMAN_TABLE(253, 0b111111111111111111111101111, 27) + HUFFMAN_TABLE(254, 0b111111111111111111111110000, 27) + } + goto end; + } + case 28: { + switch (info.bits) { + HUFFMAN_TABLE(2, 0b1111111111111111111111100010, 28) + HUFFMAN_TABLE(3, 0b1111111111111111111111100011, 28) + HUFFMAN_TABLE(4, 0b1111111111111111111111100100, 28) + HUFFMAN_TABLE(5, 0b1111111111111111111111100101, 28) + HUFFMAN_TABLE(6, 0b1111111111111111111111100110, 28) + HUFFMAN_TABLE(7, 0b1111111111111111111111100111, 28) + HUFFMAN_TABLE(8, 0b1111111111111111111111101000, 28) + HUFFMAN_TABLE(11, 0b1111111111111111111111101001, 28) + HUFFMAN_TABLE(12, 0b1111111111111111111111101010, 28) + HUFFMAN_TABLE(14, 0b1111111111111111111111101011, 28) + HUFFMAN_TABLE(15, 0b1111111111111111111111101100, 28) + HUFFMAN_TABLE(16, 0b1111111111111111111111101101, 28) + HUFFMAN_TABLE(17, 0b1111111111111111111111101110, 28) + HUFFMAN_TABLE(18, 0b1111111111111111111111101111, 28) + HUFFMAN_TABLE(19, 0b1111111111111111111111110000, 28) + HUFFMAN_TABLE(20, 0b1111111111111111111111110001, 28) + HUFFMAN_TABLE(21, 0b1111111111111111111111110010, 28) + HUFFMAN_TABLE(23, 0b1111111111111111111111110011, 28) + HUFFMAN_TABLE(24, 0b1111111111111111111111110100, 28) + HUFFMAN_TABLE(25, 0b1111111111111111111111110101, 28) + HUFFMAN_TABLE(26, 0b1111111111111111111111110110, 28) + HUFFMAN_TABLE(27, 0b1111111111111111111111110111, 28) + HUFFMAN_TABLE(28, 0b1111111111111111111111111000, 28) + HUFFMAN_TABLE(29, 0b1111111111111111111111111001, 28) + HUFFMAN_TABLE(30, 0b1111111111111111111111111010, 28) + HUFFMAN_TABLE(31, 0b1111111111111111111111111011, 28) + HUFFMAN_TABLE(127, 0b1111111111111111111111111100, 28) + HUFFMAN_TABLE(220, 0b1111111111111111111111111101, 28) + HUFFMAN_TABLE(249, 0b1111111111111111111111111110, 28) + } + goto end; + } + case 30: { + switch (info.bits) { + HUFFMAN_TABLE(10, 0b111111111111111111111111111100, 30) + HUFFMAN_TABLE(13, 0b111111111111111111111111111101, 30) + HUFFMAN_TABLE(22, 0b111111111111111111111111111110, 30) + HUFFMAN_TABLE(256, 0b111111111111111111111111111111, 30) + } + goto end; + } + } +end: + return uint16_t(-1); +} + +} // namespace hpack diff --git a/src/static_table.cpp b/src/static_table.cpp new file mode 100644 index 0000000..3ebcef7 --- /dev/null +++ b/src/static_table.cpp @@ -0,0 +1,103 @@ + +#include "hpack/static_table.hpp" + +#include + +namespace hpack { + +// postcondition: returns < first_unused_index() +// and 0 ('not_found') when not found +index_type static_table_t::find(std::string_view name) noexcept { +#define STATIC_TABLE_ENTRY(cppname, name2, ...) \ + if (name == name2) \ + return values::cppname; +#include "hpack/static_table.def" + return not_found; +} + +find_result_t static_table_t::find(std::string_view name, std::string_view value) noexcept { + // uses fact, that values in static table are grouped by name + find_result_t r; + r.header_name_index = find(name); + // if no value, than will not find any value anyway + if (r.header_name_index == not_found) + return r; + for (index_type i = r.header_name_index;; ++i) { + // important: last content entry has no value, so will break loop + table_entry val = get_entry(i); + if (val.name != name || val.value.empty()) + return r; + if (val.value == value) { + r.header_name_index = i; + r.value_indexed = true; + return r; + } + } + return r; +} + +// returns 'not_found' if not found +index_type static_table_t::find_by_value(std::string_view value) noexcept { +#define STATIC_TABLE_ENTRY(cppname, name, ...) \ + __VA_OPT__(if (value == std::string_view(__VA_ARGS__)) return values::cppname;) +#include "hpack/static_table.def" + return not_found; +} + +[[nodiscard]] find_result_t static_table_t::find(index_type name, std::string_view value) noexcept { + find_result_t r; + + auto fill_result = [&](CharPtrs... vars) { + r.value_indexed = ((value == std::string_view(vars)) || ...); + // 'path' + "/" must return 'path' + // but 'path_index_html' + '/' must return 'path' too + r.header_name_index = !r.value_indexed ? name : find_by_value(value); + }; + switch (name) { + default: + if (name < first_unused_index && name != not_found) + r.header_name_index = name; + return r; + case method_get: + case method_post: + fill_result("GET", "POST"); + return r; + case path: + case path_index_html: + fill_result("/", "/index.html"); + return r; + case scheme_http: + case scheme_https: + fill_result("http", "https"); + return r; + case status_200: + case status_204: + case status_206: + case status_304: + case status_400: + case status_404: + case status_500: + fill_result("200", "204", "206", "304", "400", "404", "500"); + return r; + case accept_encoding: + fill_result("gzip, deflate"); + return r; + } + return r; +} + +// precondition: index < first_unused_index && index != 0 +// .value empty if no cached +table_entry static_table_t::get_entry(index_type index) { + assert(index < first_unused_index && index != 0); + switch (index) { +#define STATIC_TABLE_ENTRY(cppname, name, ...) \ + case values::cppname: \ + return {name __VA_OPT__(, __VA_ARGS__)}; +#include "hpack/static_table.def" + default: + return {"", ""}; + } +} + +} // namespace hpack diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..bf883d0 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.05) + +add_executable(test_hpack ${CMAKE_CURRENT_SOURCE_DIR}/test_hpack.cpp) + +target_link_libraries(test_hpack PUBLIC hpacklib) + +add_test(NAME test_hpack COMMAND test_hpack) + +set_target_properties(test_hpack PROPERTIES + CMAKE_CXX_EXTENSIONS OFF + LINKER_LANGUAGE CXX + CXX_STANDARD 20 + CMAKE_CXX_STANDARD_REQUIRED ON +) diff --git a/tests/test_hpack.cpp b/tests/test_hpack.cpp new file mode 100644 index 0000000..cdfe27c --- /dev/null +++ b/tests/test_hpack.cpp @@ -0,0 +1,692 @@ +#include "hpack/hpack.hpp" + +#include +#include + +#define TEST(name) static void test_##name() +#define error_if(...) \ + if (!!(__VA_ARGS__)) { \ + exit(__LINE__); \ + } + +using hpack::decode_integer; +using hpack::encode_integer; + +static void test_number(uint32_t value_to_encode, uint8_t prefix_length, uint32_t expected_bytes_filled = 0) { + hpack::encoder enc; + + uint8_t chars[10] = {}; + uint8_t* encoded_end = encode_integer(value_to_encode, prefix_length, chars); + if (expected_bytes_filled != 0) + error_if(encoded_end - chars != expected_bytes_filled); + const uint8_t* in = chars; + uint32_t x = decode_integer(in, encoded_end, prefix_length); + error_if(x != value_to_encode); + error_if(in != encoded_end); // decoded all what encoded +} + +TEST(encode_decode_integers) { + test_number(1337, 5, 3); + test_number(10, 5, 1); + test_number(31, 5, 2); + test_number(32, 5, 2); + test_number(127, 5, 2); + test_number(128, 5, 2); + test_number(255, 8, 2); + test_number(256, 8, 2); + test_number(16383, 5, 3); + test_number(100000, 5, 4); + test_number(1048576, 5, 4); + test_number(0, 5, 1); + test_number(1, 5, 1); + test_number(std::numeric_limits::max(), 5, 6); + hpack::encoder enc; + + uint8_t chars[10] = {}; + uint8_t* encoded_end = + encode_integer(uint64_t(std::numeric_limits::max()) + 1, 6, chars); + const uint8_t* in = chars; + try { + (void)decode_integer(in, encoded_end, 6); + } catch (...) { + // must throw overflow + return; + } + error_if(true); +} + +// vector, ordering matters! +using headers_t = std::vector>; +using bytes_t = std::vector; + +// encoder by ref, because HPACK it statefull (between requests/responses) +template +static void test_encode(hpack::encoder& enc, hpack::size_type expected_dyntab_size, + headers_t headers_to_encode, bytes_t expected_encoded_bytes, + headers_t expected_dyntab_content) { + std::vector bytes(expected_encoded_bytes.size(), ~0); + auto* out = bytes.data(); + for (auto&& [name, value] : headers_to_encode) + out = enc.template encode(name, value, out); + error_if(bytes != expected_encoded_bytes); + error_if(enc.dyntab.current_size() != expected_dyntab_size); + for (auto&& [name, value] : expected_dyntab_content) { + auto res = enc.dyntab.find(name, value); + error_if(!res); + } +} + +static void test_decode(hpack::decoder& enc, hpack::size_type expected_dyntab_size, + headers_t expected_decoded_headers, bytes_t bytes_to_decode, + headers_t expected_dyntab_content) { + assert(!bytes_to_decode.empty()); + hpack::header_view hdr; + headers_t decoded; + const uint8_t* in = bytes_to_decode.data(); + auto* e = in + bytes_to_decode.size(); + while (in != e) { + enc.decode_header(in, e, hdr); + decoded.emplace_back(hdr.name.str(), hdr.value.str()); + } + error_if(decoded != expected_decoded_headers); + error_if(enc.dyntab.current_size() != expected_dyntab_size); + error_if(in - 1 != &bytes_to_decode.back()); + size_t i = hpack::static_table_t::first_unused_index; + size_t imax = i + expected_dyntab_content.size(); + for (auto&& [name, value] : expected_dyntab_content) { + error_if(!enc.dyntab.find(name, value)); + ++i; + } +} + +// https://www.rfc-editor.org/rfc/rfc7541#appendix-C.3.1 +TEST(encode_decode1) { + hpack::encoder sender(164); + hpack::decoder receiver(164); + // first request + { + headers_t headers{ + {":method", "GET"}, + {":scheme", "http"}, + {":path", "/"}, + {":authority", "www.example.com"}, + }; + headers_t cached_headers{ + {":authority", "www.example.com"}, + }; + bytes_t bytes_expected{ + 0x82, 0x86, 0x84, 0x41, 0x0f, 0x77, 0x77, 0x77, 0x2e, 0x65, + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, + }; + + test_encode(sender, 57, headers, bytes_expected, cached_headers); + + test_decode(receiver, 57, headers, bytes_expected, cached_headers); + } + // second request + { + headers_t headers{ + {":method", "GET"}, + {":scheme", "http"}, + {":path", "/"}, + {":authority", "www.example.com"}, + {"cache-control", "no-cache"}, + }; + bytes_t bytes_expected{ + 0x82, 0x86, 0x84, 0xbe, 0x58, 0x08, 0x6e, 0x6f, 0x2d, 0x63, 0x61, 0x63, 0x68, 0x65, + }; + headers_t cached_headers{ + {"cache-control", "no-cache"}, + {":authority", "www.example.com"}, + }; + test_encode(sender, 110, headers, bytes_expected, cached_headers); + test_decode(receiver, 110, headers, bytes_expected, cached_headers); + } + // third request + { + headers_t headers{ + {":method", "GET"}, + {":scheme", "https"}, + {":path", "/index.html"}, + {":authority", "www.example.com"}, + {"custom-key", "custom-value"}, + }; + bytes_t bytes_expected{0x82, 0x87, 0x85, 0xbf, 0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0c, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x2d, 0x76, 0x61, 0x6c, 0x75, 0x65}; + headers_t cached_headers{ + {"custom-key", "custom-value"}, + {"cache-control", "no-cache"}, + {":authority", "www.example.com"}, + }; + test_encode(sender, 164, headers, bytes_expected, cached_headers); + test_decode(receiver, 164, headers, bytes_expected, cached_headers); + } +} + +TEST(encode_decode_huffman1) { + hpack::encoder sender(164); + hpack::decoder receiver(164); + // first request + { + headers_t headers{ + {":method", "GET"}, + {":scheme", "http"}, + {":path", "/"}, + {":authority", "www.example.com"}, + }; + headers_t cached_headers{ + {":authority", "www.example.com"}, + }; + bytes_t bytes_expected{ + 0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff, + }; + + test_encode(sender, 57, headers, bytes_expected, cached_headers); + + test_decode(receiver, 57, headers, bytes_expected, cached_headers); + } + // second request + { + headers_t headers{ + {":method", "GET"}, + {":scheme", "http"}, + {":path", "/"}, + {":authority", "www.example.com"}, + {"cache-control", "no-cache"}, + }; + bytes_t bytes_expected{ + 0x82, 0x86, 0x84, 0xbe, 0x58, 0x86, 0xa8, 0xeb, 0x10, 0x64, 0x9c, 0xbf, + }; + headers_t cached_headers{ + {"cache-control", "no-cache"}, + {":authority", "www.example.com"}, + }; + test_encode(sender, 110, headers, bytes_expected, cached_headers); + test_decode(receiver, 110, headers, bytes_expected, cached_headers); + } + // third request + { + headers_t headers{ + {":method", "GET"}, + {":scheme", "https"}, + {":path", "/index.html"}, + {":authority", "www.example.com"}, + {"custom-key", "custom-value"}, + }; + bytes_t bytes_expected{ + 0x82, 0x87, 0x85, 0xbf, 0x40, 0x88, 0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xa9, + 0x7d, 0x7f, 0x89, 0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xb8, 0xe8, 0xb4, 0xbf, + }; + headers_t cached_headers{ + {"custom-key", "custom-value"}, + {"cache-control", "no-cache"}, + {":authority", "www.example.com"}, + }; + test_encode(sender, 164, headers, bytes_expected, cached_headers); + test_decode(receiver, 164, headers, bytes_expected, cached_headers); + } +} + +// similar to first example, but forces eviction of dynamic table entries +TEST(encode_decode_with_eviction) { + hpack::encoder sender(256); + hpack::decoder receiver(256); + // first response + { + headers_t headers{ + {":status", "302"}, + {"cache-control", "private"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"location", "https://www.example.com"}, + }; + headers_t cached_headers{ + {"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}, + {":status", "302"}, + }; + bytes_t bytes_expected = { + 0x48, 0x03, 0x33, 0x30, 0x32, 0x58, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x61, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, 0x4f, 0x63, 0x74, 0x20, + 0x32, 0x30, 0x31, 0x33, 0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, 0x31, 0x20, + 0x47, 0x4d, 0x54, 0x6e, 0x17, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, + 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, + }; + + test_encode(sender, 222, headers, bytes_expected, cached_headers); + + test_decode(receiver, 222, headers, bytes_expected, cached_headers); + } + // second response + { + headers_t headers{ + {":status", "307"}, + {"cache-control", "private"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"location", "https://www.example.com"}, + }; + headers_t cached_headers{ + {":status", "307"}, + {"location", "https://www.example.com"}, + {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"cache-control", "private"}, + }; + bytes_t bytes_expected = {0x48, 0x03, 0x33, 0x30, 0x37, 0xc1, 0xc0, 0xbf}; + + test_encode(sender, 222, headers, bytes_expected, cached_headers); + + test_decode(receiver, 222, headers, bytes_expected, cached_headers); + } + // third response + { + headers_t headers{ + {":status", "200"}, + {"cache-control", "private"}, + {"date", "Mon, 21 Oct 2013 20:13:22 GMT"}, + {"location", "https://www.example.com"}, + {"content-encoding", "gzip"}, + {"set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}, + }; + headers_t cached_headers{ + {"set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}, + {"content-encoding", "gzip"}, + {"date", "Mon, 21 Oct 2013 20:13:22 GMT"}, + }; + bytes_t bytes_expected = { + 0x88, 0xc1, 0x61, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, 0x4f, 0x63, 0x74, 0x20, 0x32, + 0x30, 0x31, 0x33, 0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, 0x32, 0x20, 0x47, 0x4d, 0x54, 0xc0, + 0x5a, 0x04, 0x67, 0x7a, 0x69, 0x70, 0x77, 0x38, 0x66, 0x6f, 0x6f, 0x3d, 0x41, 0x53, 0x44, 0x4a, 0x4b, + 0x48, 0x51, 0x4b, 0x42, 0x5a, 0x58, 0x4f, 0x51, 0x57, 0x45, 0x4f, 0x50, 0x49, 0x55, 0x41, 0x58, 0x51, + 0x57, 0x45, 0x4f, 0x49, 0x55, 0x3b, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, 0x3d, 0x33, 0x36, + 0x30, 0x30, 0x3b, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3d, 0x31}; + + test_encode(sender, 215, headers, bytes_expected, cached_headers); + + test_decode(receiver, 215, headers, bytes_expected, cached_headers); + } +} + +TEST(huffman) { + std::string_view str = "hello world"; + uint8_t buf[20]; + auto* encoded_end = hpack::encode_string(str, +buf); + const uint8_t* in = +buf; + hpack::decoded_string out; + hpack::decode_string(in, encoded_end, out); + error_if(out.str() != str); +} + +TEST(huffman_rand) { + std::mt19937 gen(155); + bytes_t bytes; + for (int i = 0; i < 1000; ++i) + bytes.push_back(std::uniform_int_distribution(0, 255)(gen)); + bytes_t encoded; + hpack::encode_string_huffman(std::string_view((char*)bytes.data(), (char*)bytes.data() + bytes.size()), + std::back_inserter(encoded)); + const uint8_t* in = encoded.data(); + hpack::decoded_string out; + hpack::decode_string(in, in + encoded.size(), out); + error_if(in != encoded.data() + encoded.size()); + error_if(out.str() != std::string_view((char*)bytes.data(), bytes.size())); +} + +TEST(huffman_table_itself){ +#define HUFFMAN_TABLE(index, bits, bitcount) \ + { error_if(hpack::huffman_decode_table_find(hpack::sym_info_t(0b##bits, bitcount)) != uint16_t(index)); } +#include "hpack/huffman_table.def" +} + +TEST(huffman_encode_eos) { + // encoded string ("!") and EOS + bytes_t bytes{ + 0x85, 0xfe, 0x3f, 0xff, 0xff, 0xff, + }; + const uint8_t* in = bytes.data(); + hpack::decoded_string decoded; + hpack::decode_string(in, in + bytes.size(), decoded); + error_if(in != bytes.data() + bytes.size()); + error_if(decoded.str() != "!"); +} + +TEST(static_table_find) { + using st = hpack::static_table_t; + using namespace hpack; +#define STATIC_TABLE_ENTRY(cppname, header_name, ...) \ + { \ + auto res = st::find(header_name, "" __VA_OPT__(__VA_ARGS__)); \ + error_if(res.header_name_index != (index_type)static_table_t::cppname); \ + error_if(res.value_indexed != (0 __VA_OPT__(+1))); \ + } +#include "hpack/static_table.def" +} + +struct test_dyntab_t { + std::deque> d; + size_t cur_size; + size_t max_size; + + test_dyntab_t(size_t max_sz) : cur_size(0), max_size(max_sz) { + } + void add_entry(std::string name, std::string value) { + size_t entry_sz = name.size() + value.size() + 32; // RFC + while (!d.empty() && entry_sz > max_size - cur_size) { + cur_size -= (d.back().first.size() + d.back().second.size() + 32); + d.pop_back(); + } + if (entry_sz > max_size) + return; + d.push_front({std::move(name), std::move(value)}); + cur_size += entry_sz; + } +}; + +static int64_t rand_int(int64_t min, int64_t max, std::mt19937& gen) { + return std::uniform_int_distribution(min, max)(gen); +} +static std::string generate_random_string(size_t length, std::mt19937& gen) { + constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyz"; + std::string random_str; + random_str.reserve(length); + for (size_t i = 0; i < length; ++i) + random_str += characters[rand_int(0, characters.size() - 1, gen)]; + return random_str; +} + +TEST(dynamic_table_indexes) { + enum { MAX_SZ = 512 }; + hpack::dynamic_table_t table(MAX_SZ); + + error_if(table.current_size() != 0); + table.add_entry("name1", "hello world"); + table.add_entry("name2", "header2"); + table.add_entry(std::string(1000, 'a'), ""); + error_if(table.current_size() != 0); + std::mt19937 gen(213214); + + test_dyntab_t test_table(MAX_SZ); + + for (int i = 0; i < 1000; ++i) { + std::string random_name = generate_random_string(rand_int(1, 300, gen), gen); + std::string random_value = generate_random_string(rand_int(0, 300, gen), gen); + table.add_entry(random_name, random_value); + auto r = table.find(random_name, random_value); + if (random_name.size() + random_value.size() + 32 <= MAX_SZ) { + // firstly inserted value always have first index + error_if(r.header_name_index != 62); + error_if(!r.value_indexed); + } else { + error_if(r.header_name_index != 0); + error_if(r.value_indexed); + } + test_table.add_entry(random_name, random_value); + error_if(table.current_size() != test_table.cur_size); + // 62 is min index of HPACK dynamic table + for (size_t j = 62; j < 62 + test_table.d.size(); ++j) { + const auto& real_entry = table.get_entry(j); + const auto& test_entry = test_table.d[j - 62]; + error_if(real_entry.name != test_entry.first); + error_if(real_entry.value != test_entry.second); + } + } +} + +TEST(tg_answer) { + std::vector bytes = { + 0x88, 0x76, 0x89, 0xaa, 0x63, 0x55, 0xe5, 0x80, 0xae, 0x17, 0x97, 0x7, 0x61, 0x96, 0xc3, 0x61, 0xbe, + 0x94, 0x3, 0x8a, 0x6e, 0x2d, 0x6a, 0x8, 0x2, 0x69, 0x40, 0x3b, 0x70, 0xf, 0x5c, 0x13, 0x4a, 0x62, + 0xd1, 0xbf, 0x5f, 0x8b, 0x1d, 0x75, 0xd0, 0x62, 0xd, 0x26, 0x3d, 0x4c, 0x74, 0x41, 0xea, 0x5c, 0x4, + 0x31, 0x39, 0x32, 0x36, 0x0, 0x91, 0x42, 0x6c, 0x31, 0x12, 0xb2, 0x6c, 0x1d, 0x48, 0xac, 0xf6, 0x25, + 0x64, 0x14, 0x96, 0xd8, 0x64, 0xfa, 0xa0, 0xa4, 0x7e, 0x56, 0x1c, 0xc5, 0x81, 0x90, 0xb6, 0xcb, 0x80, + 0x0, 0x3e, 0xd4, 0x35, 0x44, 0xa2, 0xd9, 0xb, 0xba, 0xd8, 0xef, 0x9e, 0x91, 0x9a, 0xa4, 0x7d, 0xa9, + 0x5d, 0x85, 0xa0, 0xe3, 0x93, 0x0, 0x93, 0x19, 0x8, 0x54, 0x21, 0x62, 0x1e, 0xa4, 0xd8, 0x7a, 0x16, + 0x1d, 0x14, 0x1f, 0xc2, 0xc7, 0xb0, 0xd3, 0x1a, 0xaf, 0x1, 0x2a, 0x0, 0x94, 0x19, 0x8, 0x54, 0x21, + 0x62, 0x1e, 0xa4, 0xd8, 0x7a, 0x16, 0x1d, 0x14, 0x1f, 0xc2, 0xd4, 0x95, 0x33, 0x9e, 0x44, 0x7f, 0x90, + 0xc5, 0x83, 0x7f, 0xd2, 0x9a, 0xf5, 0x6e, 0xdf, 0xf4, 0xa6, 0xad, 0x7b, 0xf2, 0x6a, 0xd3, 0xbb, 0x0, + 0x94, 0x19, 0x8, 0x54, 0x21, 0x62, 0x1e, 0xa4, 0xd8, 0x7a, 0x16, 0x2f, 0x9a, 0xce, 0x82, 0xad, 0x39, + 0x47, 0x21, 0x6c, 0x47, 0xa5, 0xbc, 0x7a, 0x92, 0x5a, 0x92, 0xb6, 0x72, 0xd5, 0x32, 0x67, 0xfa, 0xbc, + 0x7a, 0x92, 0x5a, 0x92, 0xb6, 0xff, 0x55, 0x97, 0xea, 0xf8, 0xd2, 0x5f, 0xad, 0xc5, 0xb3, 0xb9, 0x6c, + 0xfa, 0xbc, 0x7a, 0xaa, 0x29, 0x12, 0x63, 0xd5, + }; + hpack::decoder e; + headers_t expected{ + {":status", "200"}, + {"server", "nginx/1.18.0"}, + {"date", "Fri, 06 Sep 2024 07:08:24 GMT"}, + {"content-type", "application/json"}, + {"content-length", "1926"}, + {"strict-transport-security", "max-age=31536000; includeSubDomains; preload"}, + {"access-control-allow-origin", "*"}, + {"access-control-allow-methods", "GET, POST, OPTIONS"}, + {"access-control-expose-headers", "Content-Length,Content-Type,Date,Server,Connection"}, + }; + headers_t result; + const hpack::byte_t* in = bytes.data(); + error_if(200 != e.decode_response_status(in, bytes.data() + bytes.size())); + hpack::decode_headers_block(e, bytes, [&](std::string_view name, std::string_view value) { + result.emplace_back(std::string(name), std::string(value)); + }); + error_if(result != expected); +} + +TEST(decode_status) { + hpack::encoder e; + hpack::decoder de; + bytes_t rsp; + + e.encode_header_fully_indexed(hpack::static_table_t::status_304, std::back_inserter(rsp)); + const auto* in = rsp.data(); + error_if(304 != de.decode_response_status(in, rsp.data() + rsp.size())); + error_if(in != rsp.data() + rsp.size()); + rsp.clear(); + + // use status as name and valid status code + e.encode_header_without_indexing(hpack::static_table_t::status_200, "200", std::back_inserter(rsp)); + in = rsp.data(); + error_if(200 != de.decode_response_status(in, rsp.data() + rsp.size())); + error_if(in != rsp.data() + rsp.size()); + rsp.clear(); + + e.encode_header_without_indexing(hpack::static_table_t::status_200, "fds", std::back_inserter(rsp)); + in = rsp.data(); + try { + de.decode_response_status(in, rsp.data() + rsp.size()); + error_if(true); + } catch (...) { + } + rsp.clear(); + + e.encode_header_without_indexing(hpack::static_table_t::status_200, "2000", std::back_inserter(rsp)); + in = rsp.data(); + try { + de.decode_response_status(in, rsp.data() + rsp.size()); + error_if(true); + } catch (...) { + } + rsp.clear(); + + e.encode_header_never_indexing(hpack::static_table_t::status_200, "2 0 0", std::back_inserter(rsp)); + in = rsp.data(); + try { + de.decode_response_status(in, rsp.data() + rsp.size()); + error_if(true); + } catch (...) { + } + rsp.clear(); + + e.encode_header_and_cache(hpack::static_table_t::status_200, "555", std::back_inserter(rsp)); + in = rsp.data(); + error_if(555 != de.decode_response_status(in, rsp.data() + rsp.size())); + error_if(in != rsp.data() + rsp.size()); + rsp.clear(); +} + +TEST(dynamic_table_size_update) { + hpack::encoder e; + hpack::decoder de; + bytes_t bytes; + + e.encode_dynamic_table_size_update(144, std::back_inserter(bytes)); + const auto* in = bytes.data(); + const auto* end = bytes.data() + bytes.size(); + // decode dynamic table size (repeated because its in .cpp) + auto decode_size_update = hpack::decode_integer(in, end, 5); + error_if(144 != decode_size_update); + error_if(in != end); // all parsed +} + +TEST(static_table_find_by_index) { + using namespace hpack; + { + find_result_t res; + res = static_table_t::find(0, ""); + error_if(res.value_indexed || res.header_name_index); + res = static_table_t::find(static_table_t::first_unused_index, "abc"); + error_if(res.value_indexed || res.header_name_index); + } + std::string possible_values[]{ +#define STATIC_TABLE_ENTRY(cppname, header_name, ...) __VA_OPT__(__VA_ARGS__, ) +#include "hpack/static_table.def" + }; + std::string impossible_values[]{ + "", + "fdsgwrg", + "hello world", + }; + auto test_index = [&](index_type i) { + for (std::string_view val : possible_values) { + auto myentry = static_table_t::get_entry(i); + find_result_t res = static_table_t::find(i, val); + find_result_t res2 = static_table_t::find(myentry.name, val); + error_if(res.value_indexed != res2.value_indexed); + error_if(static_table_t::get_entry(res.header_name_index).name != myentry.name); + error_if(static_table_t::get_entry(res.header_name_index).name != + static_table_t::get_entry(res2.header_name_index).name); + if (val == myentry.value) + error_if(res.header_name_index != i); + } + for (std::string_view val : impossible_values) { + auto res = static_table_t::find(i, val); + error_if(res.value_indexed); + error_if(res.header_name_index != i); + } + }; + for (index_type i = 1; i < static_table_t::first_unused_index; ++i) + test_index(i); + { + auto res1 = static_table_t::find(static_table_t::path, "/"); + auto res2 = static_table_t::find(static_table_t::path_index_html, "/"); + error_if(res1.value_indexed != res2.value_indexed); + error_if(res1.header_name_index != res2.header_name_index); + error_if(res1.header_name_index != static_table_t::path); + } +} + +TEST(decoded_string) { + // empry str + + hpack::decoded_string str; + error_if(str); + error_if(str.bytes_allocated()); + str = std::move(str); + error_if(str); + error_if(str.bytes_allocated()); + error_if(str.str() != ""); + + // non huffman + + std::string_view test = "hello"; + str = test; + error_if(str.bytes_allocated()); + error_if(str.str() != test); + str.reset(); + error_if(str); + error_if(str.str() != ""); + error_if(str.bytes_allocated()); + + // huffman + + bytes_t out; + hpack::encode_string_huffman(test, std::back_inserter(out)); + const auto* in = out.data(); + hpack::decode_string(in, in + out.size(), str); + error_if(!str); + error_if(str.str() != test); + error_if(str.bytes_allocated() != std::bit_ceil(test.size())); + + // memory reuse + + std::string_view before = str.str(); + in = out.data(); + hpack::decode_string(in, in + out.size(), str); + error_if(!str); + error_if(str.str() != test); + error_if(str.bytes_allocated() != std::bit_ceil(test.size())); + error_if(before.data() != str.str().data()); + + // memory reuse for smaller string + + std::string_view test2 = "ab"; + bytes_t out2; + hpack::encode_string_huffman(test2, std::back_inserter(out2)); + in = out2.data(); + + hpack::decode_string(in, in + out2.size(), str); + error_if(!str); + error_if(str != test2); + error_if(before.data() != str.str().data()); + error_if(str.bytes_allocated() != std::bit_ceil(test.size())); + + // reallocate after bigger string + + std::string_view test3 = "hello world big string"; + bytes_t out3; + hpack::encode_string_huffman(test3, std::back_inserter(out3)); + in = out3.data(); + hpack::decode_string(in, in + out3.size(), str); + + error_if(!str); + error_if(str != test3); + error_if(str.bytes_allocated() != std::bit_ceil(test3.size())); + + // zero-len str huffman + + bytes_t out_empty; + hpack::encode_string_huffman("", std::back_inserter(out_empty)); + in = out_empty.data(); + hpack::decode_string(in, in + out_empty.size(), str); + error_if(str); + error_if(str.bytes_allocated() != std::bit_ceil(test3.size())); + error_if(str != ""); + + // reseting + + str.reset(); + + error_if(str); + error_if(str.bytes_allocated()); + error_if(str != ""); + + str.reset(); + + error_if(str != str); +} + +int main() { + test_decoded_string(); + test_tg_answer(); + test_encode_decode_with_eviction(); + test_encode_decode_huffman1(); + test_encode_decode_integers(); + test_encode_decode1(); + test_huffman_table_itself(); + test_huffman(); + test_huffman_rand(); + test_huffman_encode_eos(); + test_static_table_find(); + test_dynamic_table_indexes(); + test_decode_status(); + test_dynamic_table_size_update(); + test_static_table_find_by_index(); +}