blob: a7ba9e488762a5f26d041b6c5a2d26bb85d62430 [file] [log] [blame]
// Copyright (c) 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Decoder to test QPACK Offline Interop corpus
//
// See https://github.com/quicwg/base-drafts/wiki/QPACK-Offline-Interop for
// description of test data format.
//
// Example usage
//
// cd $TEST_DATA
// git clone https://github.com/qpackers/qifs.git
// TEST_ENCODED_DATA=$TEST_DATA/qifs/encoded/qpack-06
// TEST_QIF_DATA=$TEST_DATA/qifs/qifs
// $BIN/qpack_offline_decoder \
// $TEST_ENCODED_DATA/f5/fb-req.qifencoded.4096.100.0 \
// $TEST_QIF_DATA/fb-req.qif
// $TEST_ENCODED_DATA/h2o/fb-req-hq.out.512.0.1 \
// $TEST_QIF_DATA/fb-req-hq.qif
// $TEST_ENCODED_DATA/ls-qpack/fb-resp-hq.out.0.0.0 \
// $TEST_QIF_DATA/fb-resp-hq.qif
// $TEST_ENCODED_DATA/proxygen/netbsd.qif.proxygen.out.4096.0.0 \
// $TEST_QIF_DATA/netbsd.qif
//
#include "net/third_party/quiche/src/quic/test_tools/qpack/qpack_offline_decoder.h"
#include <cstdint>
#include <string>
#include <utility>
#include "net/third_party/quiche/src/quic/core/quic_types.h"
#include "net/third_party/quiche/src/quic/platform/api/quic_file_utils.h"
#include "net/third_party/quiche/src/quic/platform/api/quic_logging.h"
#include "net/third_party/quiche/src/quic/test_tools/qpack/qpack_test_utils.h"
#include "net/third_party/quiche/src/common/platform/api/quiche_endian.h"
#include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h"
#include "net/third_party/quiche/src/common/platform/api/quiche_text_utils.h"
namespace quic {
QpackOfflineDecoder::QpackOfflineDecoder()
: encoder_stream_error_detected_(false) {}
bool QpackOfflineDecoder::DecodeAndVerifyOfflineData(
quiche::QuicheStringPiece input_filename,
quiche::QuicheStringPiece expected_headers_filename) {
if (!ParseInputFilename(input_filename)) {
QUIC_LOG(ERROR) << "Error parsing input filename " << input_filename;
return false;
}
if (!DecodeHeaderBlocksFromFile(input_filename)) {
QUIC_LOG(ERROR) << "Error decoding header blocks in " << input_filename;
return false;
}
if (!VerifyDecodedHeaderLists(expected_headers_filename)) {
QUIC_LOG(ERROR) << "Header lists decoded from " << input_filename
<< " to not match expected headers parsed from "
<< expected_headers_filename;
return false;
}
return true;
}
void QpackOfflineDecoder::OnEncoderStreamError(
quiche::QuicheStringPiece error_message) {
QUIC_LOG(ERROR) << "Encoder stream error: " << error_message;
encoder_stream_error_detected_ = true;
}
bool QpackOfflineDecoder::ParseInputFilename(
quiche::QuicheStringPiece input_filename) {
auto pieces = quiche::QuicheTextUtils::Split(input_filename, '.');
if (pieces.size() < 3) {
QUIC_LOG(ERROR) << "Not enough fields in input filename " << input_filename;
return false;
}
auto piece_it = pieces.rbegin();
// Acknowledgement mode: 1 for immediate, 0 for none.
bool immediate_acknowledgement = false;
if (*piece_it == "0") {
immediate_acknowledgement = false;
} else if (*piece_it == "1") {
immediate_acknowledgement = true;
} else {
QUIC_LOG(ERROR)
<< "Header acknowledgement field must be 0 or 1 in input filename "
<< input_filename;
return false;
}
++piece_it;
// Maximum allowed number of blocked streams.
uint64_t max_blocked_streams = 0;
if (!quiche::QuicheTextUtils::StringToUint64(*piece_it,
&max_blocked_streams)) {
QUIC_LOG(ERROR) << "Error parsing part of input filename \"" << *piece_it
<< "\" as an integer.";
return false;
}
++piece_it;
// Maximum Dynamic Table Capacity in bytes
uint64_t maximum_dynamic_table_capacity = 0;
if (!quiche::QuicheTextUtils::StringToUint64(
*piece_it, &maximum_dynamic_table_capacity)) {
QUIC_LOG(ERROR) << "Error parsing part of input filename \"" << *piece_it
<< "\" as an integer.";
return false;
}
qpack_decoder_ = std::make_unique<QpackDecoder>(
maximum_dynamic_table_capacity, max_blocked_streams, this);
qpack_decoder_->set_qpack_stream_sender_delegate(
&decoder_stream_sender_delegate_);
// The initial dynamic table capacity is zero according to
// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#eviction.
// However, for historical reasons, offline interop encoders use
// |maximum_dynamic_table_capacity| as initial capacity.
qpack_decoder_->OnSetDynamicTableCapacity(maximum_dynamic_table_capacity);
return true;
}
bool QpackOfflineDecoder::DecodeHeaderBlocksFromFile(
quiche::QuicheStringPiece input_filename) {
// Store data in |input_data_storage|; use a quiche::QuicheStringPiece to
// efficiently keep track of remaining portion yet to be decoded.
std::string input_data_storage;
ReadFileContents(input_filename, &input_data_storage);
quiche::QuicheStringPiece input_data(input_data_storage);
while (!input_data.empty()) {
// Parse stream_id and length.
if (input_data.size() < sizeof(uint64_t) + sizeof(uint32_t)) {
QUIC_LOG(ERROR) << "Unexpected end of input file.";
return false;
}
uint64_t stream_id = quiche::QuicheEndian::NetToHost64(
*reinterpret_cast<const uint64_t*>(input_data.data()));
input_data = input_data.substr(sizeof(uint64_t));
uint32_t length = quiche::QuicheEndian::NetToHost32(
*reinterpret_cast<const uint32_t*>(input_data.data()));
input_data = input_data.substr(sizeof(uint32_t));
if (input_data.size() < length) {
QUIC_LOG(ERROR) << "Unexpected end of input file.";
return false;
}
// Parse data.
quiche::QuicheStringPiece data = input_data.substr(0, length);
input_data = input_data.substr(length);
// Process data.
if (stream_id == 0) {
qpack_decoder_->encoder_stream_receiver()->Decode(data);
if (encoder_stream_error_detected_) {
QUIC_LOG(ERROR) << "Error detected on encoder stream.";
return false;
}
} else {
auto headers_handler = std::make_unique<test::TestHeadersHandler>();
auto progressive_decoder = qpack_decoder_->CreateProgressiveDecoder(
stream_id, headers_handler.get());
progressive_decoder->Decode(data);
progressive_decoder->EndHeaderBlock();
if (headers_handler->decoding_error_detected()) {
QUIC_LOG(ERROR) << "Sync decoding error on stream " << stream_id << ": "
<< headers_handler->error_message();
return false;
}
decoders_.push_back({std::move(headers_handler),
std::move(progressive_decoder), stream_id});
}
// Move decoded header lists from TestHeadersHandlers and append them to
// |decoded_header_lists_| while preserving the order in |decoders_|.
while (!decoders_.empty() &&
decoders_.front().headers_handler->decoding_completed()) {
Decoder* decoder = &decoders_.front();
if (decoder->headers_handler->decoding_error_detected()) {
QUIC_LOG(ERROR) << "Async decoding error on stream "
<< decoder->stream_id << ": "
<< decoder->headers_handler->error_message();
return false;
}
if (!decoder->headers_handler->decoding_completed()) {
QUIC_LOG(ERROR) << "Decoding incomplete after reading entire"
" file, on stream "
<< decoder->stream_id;
return false;
}
decoded_header_lists_.push_back(
decoder->headers_handler->ReleaseHeaderList());
decoders_.pop_front();
}
}
if (!decoders_.empty()) {
DCHECK(!decoders_.front().headers_handler->decoding_completed());
QUIC_LOG(ERROR) << "Blocked decoding uncomplete after reading entire"
" file, on stream "
<< decoders_.front().stream_id;
return false;
}
return true;
}
bool QpackOfflineDecoder::VerifyDecodedHeaderLists(
quiche::QuicheStringPiece expected_headers_filename) {
// Store data in |expected_headers_data_storage|; use a
// quiche::QuicheStringPiece to efficiently keep track of remaining portion
// yet to be decoded.
std::string expected_headers_data_storage;
ReadFileContents(expected_headers_filename, &expected_headers_data_storage);
quiche::QuicheStringPiece expected_headers_data(
expected_headers_data_storage);
while (!decoded_header_lists_.empty()) {
spdy::SpdyHeaderBlock decoded_header_list =
std::move(decoded_header_lists_.front());
decoded_header_lists_.pop_front();
spdy::SpdyHeaderBlock expected_header_list;
if (!ReadNextExpectedHeaderList(&expected_headers_data,
&expected_header_list)) {
QUIC_LOG(ERROR)
<< "Error parsing expected header list to match next decoded "
"header list.";
return false;
}
if (!CompareHeaderBlocks(std::move(decoded_header_list),
std::move(expected_header_list))) {
QUIC_LOG(ERROR) << "Decoded header does not match expected header.";
return false;
}
}
if (!expected_headers_data.empty()) {
QUIC_LOG(ERROR)
<< "Not enough encoded header lists to match expected ones.";
return false;
}
return true;
}
bool QpackOfflineDecoder::ReadNextExpectedHeaderList(
quiche::QuicheStringPiece* expected_headers_data,
spdy::SpdyHeaderBlock* expected_header_list) {
while (true) {
quiche::QuicheStringPiece::size_type endline =
expected_headers_data->find('\n');
// Even last header list must be followed by an empty line.
if (endline == quiche::QuicheStringPiece::npos) {
QUIC_LOG(ERROR) << "Unexpected end of expected header list file.";
return false;
}
if (endline == 0) {
// Empty line indicates end of header list.
*expected_headers_data = expected_headers_data->substr(1);
return true;
}
quiche::QuicheStringPiece header_field =
expected_headers_data->substr(0, endline);
auto pieces = quiche::QuicheTextUtils::Split(header_field, '\t');
if (pieces.size() != 2) {
QUIC_LOG(ERROR) << "Header key and value must be separated by TAB.";
return false;
}
expected_header_list->AppendValueOrAddHeader(pieces[0], pieces[1]);
*expected_headers_data = expected_headers_data->substr(endline + 1);
}
}
bool QpackOfflineDecoder::CompareHeaderBlocks(
spdy::SpdyHeaderBlock decoded_header_list,
spdy::SpdyHeaderBlock expected_header_list) {
if (decoded_header_list == expected_header_list) {
return true;
}
// The h2o decoder reshuffles the "content-length" header and pseudo-headers,
// see
// https://github.com/qpackers/qifs/blob/master/encoded/qpack-03/h2o/README.md.
// Remove such headers one by one if they match.
const char* kContentLength = "content-length";
const char* kPseudoHeaderPrefix = ":";
for (spdy::SpdyHeaderBlock::iterator decoded_it = decoded_header_list.begin();
decoded_it != decoded_header_list.end();) {
const quiche::QuicheStringPiece key = decoded_it->first;
if (key != kContentLength &&
!quiche::QuicheTextUtils::StartsWith(key, kPseudoHeaderPrefix)) {
++decoded_it;
continue;
}
spdy::SpdyHeaderBlock::iterator expected_it =
expected_header_list.find(key);
if (expected_it == expected_header_list.end() ||
decoded_it->second != expected_it->second) {
++decoded_it;
continue;
}
// SpdyHeaderBlock does not support erasing by iterator, only by key.
++decoded_it;
expected_header_list.erase(key);
// This will invalidate |key|.
decoded_header_list.erase(key);
}
return decoded_header_list == expected_header_list;
}
} // namespace quic