| // 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_.DecodeHeaderBlock(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; | |
| 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 |