|  | // 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 |