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