| /** @file |
| |
| Common functions for HPACK and QPACK |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include "proxy/hdrs/XPACK.h" |
| #include "proxy/hdrs/HuffmanCodec.h" |
| #include "proxy/hdrs/HdrToken.h" |
| |
| #include "tscore/Arena.h" |
| #include "tscore/Diags.h" |
| #include "tscore/ink_memory.h" |
| #include "tsutil/LocalBuffer.h" |
| #include <cstdint> |
| |
| namespace |
| { |
| DbgCtl dbg_ctl_xpack{"xpack"}; |
| |
| #define XPACKDbg(fmt, ...) Dbg(dbg_ctl_xpack, fmt, ##__VA_ARGS__) |
| |
| inline bool |
| match(const char *s1, int s1_len, const char *s2, int s2_len) |
| { |
| if (s1_len != s2_len) { |
| return false; |
| } |
| |
| if (s1 == s2) { |
| return true; |
| } |
| |
| if (memcmp(s1, s2, s1_len) != 0) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // end anonymous namespace |
| |
| // |
| // [RFC 7541] 5.1. Integer representation |
| // |
| int64_t |
| xpack_decode_integer(uint64_t &dst, const uint8_t *buf_start, const uint8_t *buf_end, uint8_t n) |
| { |
| if (buf_start >= buf_end) { |
| return XPACK_ERROR_COMPRESSION_ERROR; |
| } |
| |
| const uint8_t *p = buf_start; |
| |
| dst = (*p & ((1 << n) - 1)); |
| if (dst == static_cast<uint64_t>(1 << n) - 1) { |
| int m = 0; |
| do { |
| if (++p >= buf_end) { |
| return XPACK_ERROR_COMPRESSION_ERROR; |
| } |
| |
| uint64_t added_value = *p & 0x7f; |
| if ((UINT64_MAX >> m) < added_value) { |
| // Excessively large integer encodings - in value or octet |
| // length - MUST be treated as a decoding error. |
| return XPACK_ERROR_COMPRESSION_ERROR; |
| } |
| dst += added_value << m; |
| m += 7; |
| } while (*p & 0x80); |
| } |
| |
| return p - buf_start + 1; |
| } |
| |
| // |
| // [RFC 7541] 5.2. String Literal Representation |
| // return content from String Data (Length octets) with huffman decoding if it is encoded |
| // |
| int64_t |
| xpack_decode_string(Arena &arena, char **str, uint64_t &str_length, const uint8_t *buf_start, const uint8_t *buf_end, uint8_t n) |
| { |
| if (buf_start >= buf_end) { |
| return XPACK_ERROR_COMPRESSION_ERROR; |
| } |
| |
| const uint8_t *p = buf_start; |
| bool isHuffman = *p & (0x01 << n); |
| uint64_t encoded_string_len = 0; |
| int64_t len = 0; |
| |
| len = xpack_decode_integer(encoded_string_len, p, buf_end, n); |
| if (len == XPACK_ERROR_COMPRESSION_ERROR) { |
| return XPACK_ERROR_COMPRESSION_ERROR; |
| } |
| p += len; |
| |
| if (buf_end < p || static_cast<uint64_t>(buf_end - p) < encoded_string_len) { |
| return XPACK_ERROR_COMPRESSION_ERROR; |
| } |
| |
| if (isHuffman) { |
| // Allocate temporary area twice the size of before decoded data |
| uint32_t const str_len = encoded_string_len * 2; |
| *str = arena.str_alloc(str_len); |
| |
| len = huffman_decode(*str, str_len, p, encoded_string_len); |
| if (len < 0) { |
| return XPACK_ERROR_COMPRESSION_ERROR; |
| } |
| str_length = len; |
| } else { |
| *str = arena.str_alloc(encoded_string_len); |
| |
| memcpy(*str, reinterpret_cast<const char *>(p), encoded_string_len); |
| |
| str_length = encoded_string_len; |
| } |
| |
| return p + encoded_string_len - buf_start; |
| } |
| |
| // |
| // [RFC 7541] 5.1. Integer representation |
| // |
| int64_t |
| xpack_encode_integer(uint8_t *buf_start, const uint8_t *buf_end, uint64_t value, uint8_t n) |
| { |
| if (buf_start >= buf_end) { |
| return -1; |
| } |
| |
| uint8_t *p = buf_start; |
| |
| // Preserve the first n bits |
| uint8_t prefix = *buf_start & (0xFF << n); |
| |
| if (value < (static_cast<uint64_t>(UINT64_C(1) << n) - 1)) { |
| *(p++) = value; |
| } else { |
| *(p++) = (1 << n) - 1; |
| value -= (1 << n) - 1; |
| while (value >= 128) { |
| if (p >= buf_end) { |
| return -1; |
| } |
| *(p++) = (value & 0x7F) | 0x80; |
| value = value >> 7; |
| } |
| if (p + 1 >= buf_end) { |
| return -1; |
| } |
| *(p++) = value; |
| } |
| |
| // Restore the prefix |
| *buf_start |= prefix; |
| |
| return p - buf_start; |
| } |
| |
| int64_t |
| xpack_encode_string(uint8_t *buf_start, const uint8_t *buf_end, const char *value, uint64_t value_len, uint8_t n) |
| { |
| uint8_t *p = buf_start; |
| constexpr bool use_huffman = true; |
| |
| uint32_t const max_expected_size = value_len * 4; |
| ts::LocalBuffer<uint8_t, 4096> local_buffer(max_expected_size); |
| uint8_t *data = local_buffer.data(); |
| int64_t data_len = 0; |
| |
| // TODO Choose whether to use Huffman encoding wisely |
| // cppcheck-suppress knownConditionTrueFalse; leaving "use_huffman" for wise huffman usage in the future |
| if (use_huffman && value_len) { |
| data_len = huffman_encode(data, max_expected_size, reinterpret_cast<const uint8_t *>(value), value_len); |
| } |
| |
| if (data_len < 0) { |
| return -1; |
| } |
| |
| // Length |
| const int64_t len = xpack_encode_integer(p, buf_end, data_len, n); |
| if (len == -1) { |
| return -1; |
| } |
| |
| if (use_huffman) { |
| *p |= 0x01 << n; |
| } else { |
| *p &= ~(0x01 << n); |
| } |
| p += len; |
| |
| if (buf_end < p || buf_end - p < data_len) { |
| return -1; |
| } |
| |
| // Value |
| if (data_len) { |
| memcpy(p, data, data_len); |
| p += data_len; |
| } |
| |
| return p - buf_start; |
| } |
| |
| // |
| // DynamicTable |
| // |
| XpackDynamicTable::XpackDynamicTable(uint32_t size) : _maximum_size(size), _available(size), _max_entries(size), _storage(size) |
| { |
| XPACKDbg("Dynamic table size: %u", size); |
| this->_entries = static_cast<struct XpackDynamicTableEntry *>(ats_malloc(sizeof(struct XpackDynamicTableEntry) * size)); |
| this->_entries_head = size - 1; |
| this->_entries_tail = size - 1; |
| } |
| |
| XpackDynamicTable::~XpackDynamicTable() |
| { |
| if (this->_entries) { |
| ats_free(this->_entries); |
| this->_entries = nullptr; |
| } |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::lookup(uint32_t index, const char **name, size_t *name_len, const char **value, size_t *value_len) const |
| { |
| XPACKDbg("Lookup entry: abs_index=%u", index); |
| |
| if (this->is_empty()) { |
| // There's no entry |
| return {0, XpackLookupResult::MatchType::NONE}; |
| } |
| |
| if (index > this->_entries[this->_entries_head].index) { |
| // The index is invalid |
| return {0, XpackLookupResult::MatchType::NONE}; |
| } |
| |
| if (index < this->_entries[this->_calc_index(this->_entries_tail, 1)].index) { |
| // The index is invalid |
| return {0, XpackLookupResult::MatchType::NONE}; |
| } |
| |
| uint32_t pos = this->_calc_index(this->_entries_head, index - this->_entries[this->_entries_head].index); |
| *name_len = this->_entries[pos].name_len; |
| *value_len = this->_entries[pos].value_len; |
| this->_storage.read(this->_entries[pos].offset, name, *name_len, value, *value_len); |
| if (this->_entries[pos].wks) { |
| *name = this->_entries[pos].wks; |
| } |
| return {index, XpackLookupResult::MatchType::EXACT}; |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::lookup(const char *name, size_t name_len, const char *value, size_t value_len) const |
| { |
| XPACKDbg("Lookup entry: name=%.*s, value=%.*s", static_cast<int>(name_len), name, static_cast<int>(value_len), value); |
| XpackLookupResult::MatchType match_type = XpackLookupResult::MatchType::NONE; |
| uint32_t i = this->_calc_index(this->_entries_tail, 1); |
| uint32_t end = this->_calc_index(this->_entries_head, 1); |
| uint32_t candidate_index = 0; |
| const char *tmp_name = nullptr; |
| const char *tmp_value = nullptr; |
| |
| // DynamicTable is empty |
| if (this->is_empty()) { |
| return {candidate_index, match_type}; |
| } |
| |
| for (; i != end; i = this->_calc_index(i, 1)) { |
| if (name_len != 0 && this->_entries[i].name_len == name_len) { |
| this->_storage.read(this->_entries[i].offset, &tmp_name, this->_entries[i].name_len, &tmp_value, this->_entries[i].value_len); |
| if (match(name, name_len, tmp_name, this->_entries[i].name_len)) { |
| candidate_index = this->_entries[i].index; |
| if (match(value, value_len, tmp_value, this->_entries[i].value_len)) { |
| // Exact match |
| match_type = XpackLookupResult::MatchType::EXACT; |
| break; |
| } else { |
| // Name match -- Keep it for no exact matches |
| match_type = XpackLookupResult::MatchType::NAME; |
| } |
| } |
| } |
| } |
| |
| XPACKDbg("Lookup entry: candidate_index=%u, match_type=%u", candidate_index, static_cast<unsigned int>(match_type)); |
| return {candidate_index, match_type}; |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::lookup(const std::string_view name, const std::string_view value) const |
| { |
| return lookup(name.data(), name.length(), value.data(), value.length()); |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::lookup_relative(uint32_t relative_index, const char **name, size_t *name_len, const char **value, |
| size_t *value_len) const |
| { |
| XPACKDbg("Lookup entry: rel_index=%u", relative_index); |
| return this->lookup(this->_entries[this->_entries_head].index - relative_index, name, name_len, value, value_len); |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::lookup_relative(const char *name, size_t name_len, const char *value, size_t value_len) const |
| { |
| XpackLookupResult result = this->lookup(name, name_len, value, value_len); |
| result.index = this->_entries[this->_entries_head].index - result.index; |
| return result; |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::lookup_relative(const std::string_view name, const std::string_view value) const |
| { |
| return this->lookup_relative(name.data(), name.length(), value.data(), value.length()); |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::insert_entry(const char *name, size_t name_len, const char *value, size_t value_len) |
| { |
| if (this->_max_entries == 0) { |
| return {UINT32_C(0), XpackLookupResult::MatchType::NONE}; |
| } |
| |
| // Make enough space to insert a new entry |
| uint64_t required_size = static_cast<uint64_t>(name_len) + static_cast<uint64_t>(value_len) + ADDITIONAL_32_BYTES; |
| if (required_size > this->_available) { |
| if (!this->_make_space(required_size - this->_available)) { |
| // We can't insert a new entry because some stream(s) refer an entry that need to be evicted or the header is too big to |
| // store. This is fine with HPACK, but not with QPACK. |
| return {UINT32_C(0), XpackLookupResult::MatchType::NONE}; |
| } |
| } |
| |
| // Insert |
| const char *wks = nullptr; |
| hdrtoken_tokenize(name, name_len, &wks); |
| this->_entries_head = this->_calc_index(this->_entries_head, 1); |
| this->_entries[this->_entries_head] = { |
| this->_entries_inserted++, |
| this->_storage.write(name, static_cast<uint32_t>(name_len), value, static_cast<uint32_t>(value_len)), |
| static_cast<uint32_t>(name_len), |
| static_cast<uint32_t>(value_len), |
| 0, |
| wks}; |
| this->_available -= required_size; |
| |
| XPACKDbg("Insert Entry: entry=%u, index=%u, size=%zu", this->_entries_head, this->_entries_inserted - 1, name_len + value_len); |
| XPACKDbg("Available size: %u", this->_available); |
| return {this->_entries_inserted, value_len ? XpackLookupResult::MatchType::EXACT : XpackLookupResult::MatchType::NAME}; |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::insert_entry(const std::string_view name, const std::string_view value) |
| { |
| return insert_entry(name.data(), name.length(), value.data(), value.length()); |
| } |
| |
| const XpackLookupResult |
| XpackDynamicTable::duplicate_entry(uint32_t current_index) |
| { |
| const char *name = nullptr; |
| size_t name_len = 0; |
| const char *value = nullptr; |
| size_t value_len = 0; |
| char *duped_name; |
| char *duped_value; |
| |
| XpackLookupResult result = this->lookup(current_index, &name, &name_len, &value, &value_len); |
| if (result.match_type != XpackLookupResult::MatchType::EXACT) { |
| return result; |
| } |
| |
| // We need to dup name and value to avoid memcpy-param-overlap |
| duped_name = ats_strndup(name, name_len); |
| duped_value = ats_strndup(value, value_len); |
| result = this->insert_entry(duped_name, name_len, duped_value, value_len); |
| ats_free(duped_name); |
| ats_free(duped_value); |
| |
| return result; |
| } |
| |
| bool |
| XpackDynamicTable::should_duplicate(uint32_t /* index ATS_UNUSED */) |
| { |
| // TODO: Check whether a specified entry should be duplicated |
| // Just return false for now |
| return false; |
| } |
| |
| bool |
| XpackDynamicTable::update_maximum_size(uint32_t new_max_size) |
| { |
| uint32_t used = this->_maximum_size - this->_available; |
| if (used < new_max_size) { |
| this->_maximum_size = new_max_size; |
| this->_available = new_max_size - used; |
| this->_expand_storage_size(new_max_size); |
| return true; |
| } |
| |
| // used is larger than or equal to new_max_size. This means that _maximum_size |
| // is shrinking and we need to evict entries to get the used space below |
| // new_max_size. |
| bool ret = this->_make_space(used - new_max_size); |
| // Size update must succeed. |
| if (ret) { |
| this->_available = new_max_size - (this->_maximum_size - this->_available); |
| this->_maximum_size = new_max_size; |
| } |
| return ret; |
| } |
| |
| uint32_t |
| XpackDynamicTable::size() const |
| { |
| return this->_maximum_size - this->_available; |
| } |
| |
| uint32_t |
| XpackDynamicTable::maximum_size() const |
| { |
| return this->_maximum_size; |
| } |
| |
| void |
| XpackDynamicTable::ref_entry(uint32_t index) |
| { |
| uint32_t pos = this->_calc_index(this->_entries_head, (index - this->_entries[this->_entries_head].index)); |
| ++this->_entries[pos].ref_count; |
| } |
| |
| void |
| XpackDynamicTable::unref_entry(uint32_t index) |
| { |
| uint32_t pos = this->_calc_index(this->_entries_head, (index - this->_entries[this->_entries_head].index)); |
| --this->_entries[pos].ref_count; |
| } |
| |
| bool |
| XpackDynamicTable::is_empty() const |
| { |
| return this->_entries_head == this->_entries_tail; |
| } |
| |
| uint32_t |
| XpackDynamicTable::largest_index() const |
| { |
| // This function can return a meaningful value only if there is at least one entry on the table. |
| ink_assert(!this->is_empty()); |
| return this->_entries_inserted - 1; |
| } |
| |
| uint32_t |
| XpackDynamicTable::count() const |
| { |
| if (is_empty()) { |
| return 0; |
| } else if (this->_entries_head > this->_entries_tail) { |
| return this->_entries_head - this->_entries_tail; |
| } else { |
| return (this->_max_entries - this->_entries_tail - 1) + (this->_entries_head + 1); |
| } |
| } |
| |
| void |
| XpackDynamicTable::_expand_storage_size(uint32_t new_storage_size) |
| { |
| ExpandCapacityContext context{this->_storage, new_storage_size}; |
| if (!context.ok_to_expand()) { |
| return; |
| } |
| uint32_t i = this->_calc_index(this->_entries_tail, 1); |
| uint32_t end = this->_calc_index(this->_entries_head, 1); |
| for (; i != end; i = this->_calc_index(i, 1)) { |
| auto &entry = this->_entries[i]; |
| entry.offset = context.copy_field(entry.offset, entry.name_len + entry.value_len); |
| } |
| } |
| |
| bool |
| XpackDynamicTable::_make_space(uint64_t extra_space_needed) |
| { |
| uint32_t freed = 0; |
| uint32_t tail = this->_entries_tail; |
| |
| // Check to see if we need more space and that we have entries to evict |
| while (extra_space_needed > freed && this->_entries_head != tail) { |
| tail = this->_calc_index(tail, 1); // Move to the next entry |
| |
| if (this->_entries[tail].ref_count) { |
| break; |
| } |
| freed += this->_entries[tail].name_len + this->_entries[tail].value_len + ADDITIONAL_32_BYTES; |
| } |
| |
| // Evict |
| if (freed > 0) { |
| XPACKDbg("Evict entries: from %u to %u", this->_entries[this->_calc_index(this->_entries_tail, 1)].index, |
| this->_entries[tail - 1].index); |
| this->_available += freed; |
| this->_entries_tail = tail; |
| |
| XPACKDbg("Available size: %u", this->_available); |
| } |
| |
| return freed >= extra_space_needed; |
| } |
| |
| uint32_t |
| XpackDynamicTable::_calc_index(uint32_t base, int64_t offset) const |
| { |
| if (unlikely(this->_max_entries == 0)) { |
| return base + offset; |
| } else { |
| return (base + offset) % this->_max_entries; |
| } |
| } |
| |
| // |
| // DynamicTableStorage |
| // |
| |
| XpackDynamicTableStorage::XpackDynamicTableStorage(uint32_t size) |
| : _overwrite_threshold(size), _capacity{size * 2}, _head{_capacity - 1} |
| { |
| this->_data = reinterpret_cast<uint8_t *>(ats_malloc(this->_capacity)); |
| } |
| |
| XpackDynamicTableStorage::~XpackDynamicTableStorage() |
| { |
| ats_free(this->_data); |
| this->_data = nullptr; |
| } |
| |
| void |
| XpackDynamicTableStorage::read(uint32_t offset, const char **name, uint32_t name_len, const char **value, |
| uint32_t /* value_len ATS_UNUSED */) const |
| { |
| *name = reinterpret_cast<const char *>(this->_data + offset); |
| *value = reinterpret_cast<const char *>(this->_data + offset + name_len); |
| } |
| |
| uint32_t |
| XpackDynamicTableStorage::write(const char *name, uint32_t name_len, const char *value, uint32_t value_len) |
| { |
| // insert_entry should guard against buffer overrun, but rather than overrun |
| // our buffer we assert here in case something horrible went wrong. |
| ink_release_assert(name_len + value_len <= this->_capacity); |
| ink_release_assert(this->_head == this->_capacity - 1 || this->_head + name_len + value_len <= this->_capacity); |
| |
| uint32_t offset = (this->_head + 1) % this->_capacity; |
| if (name_len > 0) { |
| memcpy(this->_data + offset, name, name_len); |
| } |
| if (value_len > 0) { |
| memcpy(this->_data + offset + name_len, value, value_len); |
| } |
| |
| this->_head = (this->_head + (name_len + value_len)) % this->_capacity; |
| if (this->_head > this->_overwrite_threshold) { |
| // This is how we wrap back around to the beginning of the buffer. |
| this->_head = this->_capacity - 1; |
| } |
| |
| return offset; |
| } |
| |
| bool |
| XpackDynamicTableStorage::_start_expanding_capacity(uint32_t new_maximum_size) |
| { |
| if ((new_maximum_size * 2) <= this->_capacity) { |
| // We never shrink our memory for the data storage because we don't support |
| // arbitrary eviction from it via XpackDynamicTableStorage. The |
| // XpackDynamicTable class keeps track of field sizes and therefore can |
| // evict properly. Also, we don't want to invalidate XpackDynamicTable's |
| // offsets into the storage. |
| return false; |
| } |
| this->_capacity = new_maximum_size * 2; |
| |
| this->_old_data = this->_data; |
| this->_data = reinterpret_cast<uint8_t *>(ats_malloc(this->_capacity)); |
| if (this->_data == nullptr) { |
| // Realloc failed. We're out of memory and ATS is in trouble. |
| return false; |
| } |
| |
| this->_overwrite_threshold = new_maximum_size; |
| this->_head = this->_capacity - 1; |
| return true; |
| } |
| |
| void |
| XpackDynamicTableStorage::_finish_expanding_capacity() |
| { |
| ats_free(this->_old_data); |
| this->_old_data = nullptr; |
| } |
| |
| uint32_t |
| ExpandCapacityContext::copy_field(uint32_t offset, uint32_t len) |
| { |
| char const *field = reinterpret_cast<char const *>(this->_storage._old_data + offset); |
| return this->_storage.write(field, len, nullptr, 0); |
| } |