blob: 1064c789167b185036ce307814d246cc3e668a54 [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.
#include "net/third_party/quiche/src/quic/core/qpack/offline/qpack_offline_decoder.h"
#include <cstdint>
#include <string>
#include <utility>
#include "net/third_party/quiche/src/quic/core/qpack/qpack_test_utils.h"
#include "net/third_party/quiche/src/quic/core/quic_types.h"
#include "net/third_party/quiche/src/quic/platform/api/quic_endian.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/platform/api/quic_text_utils.h"
namespace quic {
QpackOfflineDecoder::QpackOfflineDecoder()
: encoder_stream_error_detected_(false),
decoder_(this, &decoder_stream_sender_delegate_) {}
bool QpackOfflineDecoder::DecodeAndVerifyOfflineData(
QuicStringPiece input_filename,
QuicStringPiece 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(QuicStringPiece error_message) {
QUIC_LOG(ERROR) << "Encoder stream error: " << error_message;
encoder_stream_error_detected_ = true;
}
bool QpackOfflineDecoder::ParseInputFilename(QuicStringPiece input_filename) {
auto pieces = QuicTextUtils::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 (!QuicTextUtils::StringToUint64(*piece_it, &max_blocked_streams)) {
QUIC_LOG(ERROR) << "Error parsing part of input filename \"" << *piece_it
<< "\" as an integer.";
return false;
}
if (max_blocked_streams > 0) {
// TODO(bnc): Implement blocked streams.
QUIC_LOG(ERROR) << "Blocked streams not implemented.";
return false;
}
++piece_it;
// Dynamic Table Size in bytes
uint64_t dynamic_table_size = 0;
if (!QuicTextUtils::StringToUint64(*piece_it, &dynamic_table_size)) {
QUIC_LOG(ERROR) << "Error parsing part of input filename \"" << *piece_it
<< "\" as an integer.";
return false;
}
decoder_.SetMaximumDynamicTableCapacity(dynamic_table_size);
return true;
}
bool QpackOfflineDecoder::DecodeHeaderBlocksFromFile(
QuicStringPiece input_filename) {
// Store data in |input_data_storage|; use a QuicStringPiece to efficiently
// keep track of remaining portion yet to be decoded.
std::string input_data_storage;
ReadFileContents(input_filename, &input_data_storage);
QuicStringPiece input_data(input_data_storage);
while (!input_data.empty()) {
if (input_data.size() < sizeof(uint64_t) + sizeof(uint32_t)) {
QUIC_LOG(ERROR) << "Unexpected end of input file.";
return false;
}
uint64_t stream_id = QuicEndian::NetToHost64(
*reinterpret_cast<const uint64_t*>(input_data.data()));
input_data = input_data.substr(sizeof(uint64_t));
uint32_t length = QuicEndian::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;
}
QuicStringPiece data = input_data.substr(0, length);
input_data = input_data.substr(length);
if (stream_id == 0) {
decoder_.DecodeEncoderStreamData(data);
if (encoder_stream_error_detected_) {
QUIC_LOG(ERROR) << "Error detected on encoder stream.";
return false;
}
continue;
}
test::TestHeadersHandler headers_handler;
auto progressive_decoder =
decoder_.CreateProgressiveDecoder(stream_id, &headers_handler);
progressive_decoder->Decode(data);
progressive_decoder->EndHeaderBlock();
if (headers_handler.decoding_error_detected()) {
QUIC_LOG(ERROR) << "Decoding error on stream " << stream_id << ": "
<< headers_handler.error_message();
return false;
}
decoded_header_lists_.push_back(headers_handler.ReleaseHeaderList());
}
return true;
}
bool QpackOfflineDecoder::VerifyDecodedHeaderLists(
QuicStringPiece expected_headers_filename) {
// Store data in |expected_headers_data_storage|; use a QuicStringPiece 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);
QuicStringPiece 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(
QuicStringPiece* expected_headers_data,
spdy::SpdyHeaderBlock* expected_header_list) {
while (true) {
QuicStringPiece::size_type endline = expected_headers_data->find('\n');
// Even last header list must be followed by an empty line.
if (endline == QuicStringPiece::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;
}
QuicStringPiece header_field = expected_headers_data->substr(0, endline);
auto pieces = QuicTextUtils::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 QuicStringPiece key = decoded_it->first;
if (key != kContentLength &&
!QuicTextUtils::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