| // Copyright 2019 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 "services/data_decoder/bundled_exchanges_parser.h" |
| |
| #include <algorithm> |
| |
| #include "base/big_endian.h" |
| #include "base/bind_helpers.h" |
| #include "base/callback.h" |
| #include "base/containers/span.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/numerics/checked_math.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "components/cbor/reader.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/http/http_util.h" |
| |
| namespace data_decoder { |
| |
| namespace { |
| |
| // The maximum length of the CBOR item header (type and argument). |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#parse-type-argument |
| constexpr uint64_t kMaxCBORItemHeaderSize = 9; |
| |
| // The maximum size of the section-lengths CBOR item. |
| constexpr uint64_t kMaxSectionLengthsCBORSize = 8192; |
| |
| // The maximum size of the index section allowed in this implementation. |
| constexpr uint64_t kMaxIndexSectionSize = 1 * 1024 * 1024; |
| |
| // The maximum size of the response header CBOR. |
| constexpr uint64_t kMaxResponseHeaderLength = 512 * 1024; |
| |
| // The initial buffer size for reading an item from the response section. |
| constexpr uint64_t kInitialBufferSizeForResponse = 4096; |
| |
| const uint8_t kBundleMagicBytes[] = { |
| 0x84, 0x48, 0xF0, 0x9F, 0x8C, 0x90, 0xF0, 0x9F, 0x93, 0xA6, |
| }; |
| |
| // Section names. |
| constexpr char kIndexSection[] = "index"; |
| constexpr char kResponsesSection[] = "responses"; |
| |
| // https://tools.ietf.org/html/draft-ietf-cbor-7049bis-05#section-3.1 |
| enum class CBORType { |
| kByteString = 2, |
| kArray = 4, |
| }; |
| |
| // A list of (section-name, length) pairs. |
| using SectionLengths = std::vector<std::pair<std::string, uint64_t>>; |
| |
| // Parses a `section-lengths` CBOR item. |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#load-metadata |
| base::Optional<SectionLengths> ParseSectionLengths( |
| base::span<const uint8_t> data) { |
| cbor::Reader::DecoderError error; |
| base::Optional<cbor::Value> value = cbor::Reader::Read(data, &error); |
| if (!value.has_value() || !value->is_array()) |
| return base::nullopt; |
| |
| const cbor::Value::ArrayValue& array = value->GetArray(); |
| if (array.size() % 2 != 0) |
| return base::nullopt; |
| |
| SectionLengths result; |
| for (size_t i = 0; i < array.size(); i += 2) { |
| if (!array[i].is_string() || !array[i + 1].is_unsigned()) |
| return base::nullopt; |
| result.emplace_back(array[i].GetString(), array[i + 1].GetUnsigned()); |
| } |
| return result; |
| } |
| |
| struct ParsedHeaders { |
| base::flat_map<std::string, std::string> headers; |
| base::flat_map<std::string, std::string> pseudos; |
| }; |
| |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#cbor-headers |
| base::Optional<ParsedHeaders> ConvertCBORValueToHeaders( |
| const cbor::Value& headers_value) { |
| // Step 1. If item doesn’t match the headers rule in the above CDDL, return |
| // an error. |
| if (!headers_value.is_map()) |
| return base::nullopt; |
| |
| // Step 2. Let headers be a new header list ([FETCH]). |
| // Step 3. Let pseudos be an empty map ([INFRA]). |
| ParsedHeaders result; |
| |
| // Step 4. For each pair (name, value) in item: |
| for (const auto& item : headers_value.GetMap()) { |
| if (!item.first.is_bytestring() || !item.second.is_bytestring()) |
| return base::nullopt; |
| base::StringPiece name = item.first.GetBytestringAsString(); |
| base::StringPiece value = item.second.GetBytestringAsString(); |
| |
| // Step 4.1. If name contains any upper-case or non-ASCII characters, return |
| // an error. This matches the requirement in Section 8.1.2 of [RFC7540]. |
| if (!base::IsStringASCII(name) || |
| std::any_of(name.begin(), name.end(), base::IsAsciiUpper<char>)) |
| return base::nullopt; |
| |
| // Step 4.2. If name starts with a ‘:’: |
| if (!name.empty() && name[0] == ':') { |
| // Step 4.2.1. Assert: pseudos[name] does not exist, because CBOR maps |
| // cannot contain duplicate keys. |
| // This is ensured by cbor::Reader. |
| DCHECK(!result.pseudos.contains(name)); |
| // Step 4.2.2. Set pseudos[name] to value. |
| result.pseudos.insert( |
| std::make_pair(name.as_string(), value.as_string())); |
| // Step 4.3.3. Continue. |
| continue; |
| } |
| |
| // Step 4.3. If name or value doesn’t satisfy the requirements for a header |
| // in [FETCH], return an error. |
| if (!net::HttpUtil::IsValidHeaderName(name) || |
| !net::HttpUtil::IsValidHeaderValue(value)) |
| return base::nullopt; |
| |
| // Step 4.4. Assert: headers does not contain ([FETCH]) name, because CBOR |
| // maps cannot contain duplicate keys and an earlier step rejected |
| // upper-case bytes. |
| // This is ensured by cbor::Reader. |
| DCHECK(!result.headers.contains(name)); |
| |
| // Step 4.5. Append (name, value) to headers. |
| result.headers.insert(std::make_pair(name.as_string(), value.as_string())); |
| } |
| |
| // Step 5. Return (headers, pseudos). |
| return result; |
| } |
| |
| // A utility class for reading various values from input buffer. |
| class InputReader { |
| public: |
| explicit InputReader(base::span<const uint8_t> buf) : buf_(buf) {} |
| |
| uint64_t CurrentOffset() const { return current_offset_; } |
| size_t Size() const { return buf_.size(); } |
| |
| base::Optional<uint8_t> ReadByte() { |
| if (buf_.empty()) |
| return base::nullopt; |
| uint8_t byte = buf_[0]; |
| Advance(1); |
| return byte; |
| } |
| |
| template <typename T> |
| bool ReadBigEndian(T* out) { |
| auto bytes = ReadBytes(sizeof(T)); |
| if (!bytes) |
| return false; |
| base::ReadBigEndian(reinterpret_cast<const char*>(bytes->data()), out); |
| return true; |
| } |
| |
| base::Optional<base::span<const uint8_t>> ReadBytes(size_t n) { |
| if (buf_.size() < n) |
| return base::nullopt; |
| auto result = buf_.subspan(0, n); |
| Advance(n); |
| return result; |
| } |
| |
| // Parses the type and argument of a CBOR item from the input head. If parsed |
| // successfully and the type matches |expected_type|, returns the argument. |
| // Otherwise returns nullopt. |
| base::Optional<uint64_t> ReadCBORHeader(CBORType expected_type) { |
| auto pair = ReadTypeAndArgument(); |
| if (!pair || pair->first != expected_type) |
| return base::nullopt; |
| return pair->second; |
| } |
| |
| private: |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#parse-type-argument |
| base::Optional<std::pair<CBORType, uint64_t>> ReadTypeAndArgument() { |
| base::Optional<uint8_t> first_byte = ReadByte(); |
| if (!first_byte) |
| return base::nullopt; |
| |
| CBORType type = static_cast<CBORType>((*first_byte & 0xE0) / 0x20); |
| uint8_t b = *first_byte & 0x1F; |
| |
| if (b <= 23) |
| return std::make_pair(type, b); |
| if (b == 24) { |
| auto content = ReadByte(); |
| if (!content || *content < 24) |
| return base::nullopt; |
| return std::make_pair(type, *content); |
| } |
| if (b == 25) { |
| uint16_t content; |
| if (!ReadBigEndian(&content) || content >> 8 == 0) |
| return base::nullopt; |
| return std::make_pair(type, content); |
| } |
| if (b == 26) { |
| uint32_t content; |
| if (!ReadBigEndian(&content) || content >> 16 == 0) |
| return base::nullopt; |
| return std::make_pair(type, content); |
| } |
| if (b == 27) { |
| uint64_t content; |
| if (!ReadBigEndian(&content) || content >> 32 == 0) |
| return base::nullopt; |
| return std::make_pair(type, content); |
| } |
| return base::nullopt; |
| } |
| |
| void Advance(size_t n) { |
| DCHECK_LE(n, buf_.size()); |
| buf_ = buf_.subspan(n); |
| current_offset_ += n; |
| } |
| |
| base::span<const uint8_t> buf_; |
| uint64_t current_offset_ = 0; |
| |
| DISALLOW_COPY_AND_ASSIGN(InputReader); |
| }; |
| |
| } // namespace |
| |
| class BundledExchangesParser::SharedBundleDataSource::Observer { |
| public: |
| Observer() {} |
| virtual ~Observer() {} |
| virtual void OnDisconnect() = 0; |
| |
| DISALLOW_COPY_AND_ASSIGN(Observer); |
| }; |
| |
| // A parser for bundle's metadata. This currently looks only at the "index" |
| // section. This class owns itself and will self destruct after calling the |
| // ParseMetadataCallback. |
| // TODO(crbug.com/966753): Add support for the "manifest" section and "critical" |
| // section. |
| class BundledExchangesParser::MetadataParser |
| : BundledExchangesParser::SharedBundleDataSource::Observer { |
| public: |
| MetadataParser(scoped_refptr<SharedBundleDataSource> data_source, |
| ParseMetadataCallback callback) |
| : data_source_(data_source), callback_(std::move(callback)) { |
| data_source_->AddObserver(this); |
| } |
| ~MetadataParser() override { data_source_->RemoveObserver(this); } |
| |
| void Start() { |
| data_source_->GetSize(base::BindOnce(&MetadataParser::DidGetSize, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| private: |
| void DidGetSize(uint64_t size) { |
| size_ = size; |
| |
| // In the next step, we will parse `magic`, `section-lengths`, and the CBOR |
| // header of `sections`. |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#top-level |
| const uint64_t length = |
| std::min(size, sizeof(kBundleMagicBytes) + kMaxSectionLengthsCBORSize + |
| kMaxCBORItemHeaderSize * 2); |
| data_source_->Read(0, length, |
| base::BindOnce(&MetadataParser::ParseBundleHeader, |
| weak_factory_.GetWeakPtr(), length)); |
| } |
| |
| // Step 1-15 of |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#load-metadata |
| void ParseBundleHeader(uint64_t expected_data_length, |
| const base::Optional<std::vector<uint8_t>>& data) { |
| if (!data || data->size() != expected_data_length) { |
| RunErrorCallbackAndDestroy("Error reading bundle header."); |
| return; |
| } |
| |
| // Step 1. "Seek to offset 0 in stream. Assert: this operation doesn't |
| // fail." |
| InputReader input(*data); |
| |
| // Step 2. "If reading 10 bytes from stream returns an error or doesn't |
| // return the bytes with hex encoding "84 48 F0 9F 8C 90 F0 9F 93 A6" |
| // (the CBOR encoding of the 4-item array initial byte and 8-byte bytestring |
| // initial byte, followed by 🌐📦 in UTF-8), return an error." |
| const auto magic = input.ReadBytes(sizeof(kBundleMagicBytes)); |
| if (!magic || |
| !std::equal(magic->begin(), magic->end(), std::begin(kBundleMagicBytes), |
| std::end(kBundleMagicBytes))) { |
| RunErrorCallbackAndDestroy("Wrong magic bytes."); |
| return; |
| } |
| |
| // Step 3. "Let sectionLengthsLength be the result of getting the length of |
| // the CBOR bytestring header from stream (Section 3.5.2). If this is an |
| // error, return that error." |
| const auto section_lengths_length = |
| input.ReadCBORHeader(CBORType::kByteString); |
| if (!section_lengths_length) { |
| RunErrorCallbackAndDestroy("Cannot parse the size of section-lengths."); |
| return; |
| } |
| // Step 4. "If sectionLengthsLength is 8192 (8*1024) or greater, return an |
| // error." |
| if (*section_lengths_length >= kMaxSectionLengthsCBORSize) { |
| RunErrorCallbackAndDestroy( |
| "The section-lengths CBOR must be smaller than 8192 bytes."); |
| return; |
| } |
| |
| // Step 5. "Let sectionLengthsBytes be the result of reading |
| // sectionLengthsLength bytes from stream. If sectionLengthsBytes is an |
| // error, return that error." |
| const auto section_lengths_bytes = input.ReadBytes(*section_lengths_length); |
| if (!section_lengths_bytes) { |
| RunErrorCallbackAndDestroy("Cannot read section-lengths."); |
| return; |
| } |
| |
| // Step 6. "Let sectionLengths be the result of parsing one CBOR item |
| // (Section 3.5) from sectionLengthsBytes, matching the section-lengths |
| // rule in the CDDL ([I-D.ietf-cbor-cddl]) above. If sectionLengths is an |
| // error, return an error." |
| const auto section_lengths = ParseSectionLengths(*section_lengths_bytes); |
| if (!section_lengths) { |
| RunErrorCallbackAndDestroy("Cannot parse section-lengths."); |
| return; |
| } |
| |
| // Step 7. "Let (sectionsType, numSections) be the result of parsing the |
| // type and argument of a CBOR item from stream (Section 3.5.3)." |
| const auto num_sections = input.ReadCBORHeader(CBORType::kArray); |
| if (!num_sections) { |
| RunErrorCallbackAndDestroy("Cannot parse the number of sections."); |
| return; |
| } |
| |
| // Step 8. "If sectionsType is not 4 (a CBOR array) or numSections is not |
| // half of the length of sectionLengths, return an error." |
| if (*num_sections != section_lengths->size()) { |
| RunErrorCallbackAndDestroy("Unexpected number of sections."); |
| return; |
| } |
| |
| // Step 9. "Let sectionsStart be the current offset within stream." |
| const uint64_t sections_start = input.CurrentOffset(); |
| |
| // Step 10. "Let knownSections be the subset of the Section 6.2 that this |
| // client has implemented." |
| // Step 11. "Let ignoredSections be an empty set." |
| |
| // This implementation doesn't use knownSections nor ignoredSections. |
| |
| // Step 12. "Let sectionOffsets be an empty map ([INFRA]) from section names |
| // to (offset, length) pairs. These offsets are relative to the start of |
| // stream." |
| |
| // |section_offsets_| is defined as a class member field. |
| |
| // Step 13. "Let currentOffset be sectionsStart." |
| uint64_t current_offset = sections_start; |
| |
| // Step 14. "For each ("name", length) pair of adjacent elements in |
| // sectionLengths:" |
| for (const auto& pair : *section_lengths) { |
| const std::string& name = pair.first; |
| const uint64_t length = pair.second; |
| // Step 14.1. "If "name"'s specification in knownSections says not to |
| // process other sections, add those sections' names to ignoredSections." |
| |
| // There're no such sections at the moment. |
| |
| // Step 14.2. "If sectionOffsets["name"] exists, return an error. That is, |
| // duplicate sections are forbidden." |
| // Step 14.3. "Set sectionOffsets["name"] to (currentOffset, length)." |
| bool added = section_offsets_ |
| .insert(std::make_pair( |
| name, std::make_pair(current_offset, length))) |
| .second; |
| if (!added) { |
| RunErrorCallbackAndDestroy("Duplicated section."); |
| return; |
| } |
| |
| // Step 14.4. "Set currentOffset to currentOffset + length." |
| if (!base::CheckAdd(current_offset, length) |
| .AssignIfValid(¤t_offset) || |
| current_offset > size_) { |
| RunErrorCallbackAndDestroy("Section doesn't fit in the bundle."); |
| return; |
| } |
| } |
| |
| // Step 15. "If the "responses" section is not last in sectionLengths, |
| // return an error. This allows a streaming parser to assume that it'll |
| // know the requests by the time their responses arrive." |
| if (section_lengths->empty() || |
| section_lengths->back().first != kResponsesSection) { |
| RunErrorCallbackAndDestroy( |
| "Responses section is not the last in section-lengths."); |
| return; |
| } |
| |
| // Read the index section. |
| auto index_section = section_offsets_.find(kIndexSection); |
| if (index_section == section_offsets_.end()) { |
| RunErrorCallbackAndDestroy("No index section."); |
| return; |
| } |
| const uint64_t index_section_offset = index_section->second.first; |
| const uint64_t index_section_length = index_section->second.second; |
| |
| if (index_section_length > kMaxIndexSectionSize) { |
| RunErrorCallbackAndDestroy( |
| "Index section larger than 1MB is not supported."); |
| return; |
| } |
| |
| data_source_->Read( |
| index_section_offset, index_section_length, |
| base::BindOnce(&MetadataParser::ParseIndexSection, |
| weak_factory_.GetWeakPtr(), index_section_length)); |
| } |
| |
| // Step 1. of |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#index-section |
| void ParseIndexSection(uint64_t expected_data_length, |
| const base::Optional<std::vector<uint8_t>>& data) { |
| if (!data || data->size() != expected_data_length) { |
| RunErrorCallbackAndDestroy("Error reading index section."); |
| return; |
| } |
| |
| // Step 1. "Let index be the result of parsing sectionContents as a CBOR |
| // item matching the index rule in the above CDDL (Section 3.5). If index is |
| // an error, return an error." |
| cbor::Reader::DecoderError error; |
| base::Optional<cbor::Value> index_section_value = |
| cbor::Reader::Read(*data, &error); |
| if (!index_section_value) { |
| VLOG(1) << cbor::Reader::ErrorCodeToString(error); |
| RunErrorCallbackAndDestroy("Error parsing index section."); |
| return; |
| } |
| if (!index_section_value->is_array()) { |
| RunErrorCallbackAndDestroy("Index section must be an array."); |
| return; |
| } |
| |
| // Read the header of the response section. |
| auto responses_section = section_offsets_.find(kResponsesSection); |
| DCHECK(responses_section != section_offsets_.end()); |
| const uint64_t responses_section_offset = responses_section->second.first; |
| const uint64_t responses_section_length = responses_section->second.second; |
| |
| const uint64_t length = |
| std::min(responses_section_length, kMaxCBORItemHeaderSize); |
| data_source_->Read( |
| responses_section_offset, length, |
| base::BindOnce(&MetadataParser::ParseResponsesSectionHeader, |
| weak_factory_.GetWeakPtr(), |
| std::move(*index_section_value), length)); |
| } |
| |
| // Step 2- of |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#index-section |
| void ParseResponsesSectionHeader( |
| cbor::Value index_section_value, |
| uint64_t expected_data_length, |
| const base::Optional<std::vector<uint8_t>>& data) { |
| const cbor::Value::ArrayValue& index_section_array = |
| index_section_value.GetArray(); |
| |
| // Step 2.1. "Seek to offset sectionOffsets["responses"].offset in stream. |
| // If this fails, return an error." |
| if (!data || data->size() != expected_data_length) { |
| RunErrorCallbackAndDestroy("Error reading responses section header."); |
| return; |
| } |
| InputReader input(*data); |
| |
| // Step 2.2. "Let (responsesType, numResponses) be the result of parsing the |
| // type and argument of a CBOR item from the stream (Section 3.5.3). If this |
| // returns an error, return that error." |
| auto num_responses = input.ReadCBORHeader(CBORType::kArray); |
| if (!num_responses) { |
| RunErrorCallbackAndDestroy( |
| "Cannot parse the number of elements of the responses section."); |
| return; |
| } |
| // Step 2.3. "If responsesType is not 4 (a CBOR array) or numResponses is |
| // not half of the length of index, return an error." |
| if (index_section_array.size() % 2 != 0 || |
| *num_responses != index_section_array.size() / 2) { |
| RunErrorCallbackAndDestroy( |
| "Wrong number of elements in the responses section."); |
| return; |
| } |
| |
| // Step 3. "Let currentOffset be the current offset within stream minus |
| // sectionOffsets["responses"].offset." |
| uint64_t current_offset = input.CurrentOffset(); |
| |
| // Step 4. "Let requests be an initially-empty map ([INFRA]) from HTTP |
| // requests ([FETCH]) to structs ([INFRA]) with items named "offset" and |
| // "length"." |
| |
| // Step 5. "For each (cbor-http-request, length) pair of adjacent elements |
| // in index:" |
| for (size_t i = 0; i < index_section_array.size(); i += 2) { |
| const cbor::Value& cbor_http_request_value = index_section_array[i]; |
| const cbor::Value& length_value = index_section_array[i + 1]; |
| |
| // Step 5.1. "Let (headers, pseudos) be the result of converting |
| // cbor-http-request to a header list and pseudoheaders using the |
| // algorithm in Section 3.6. If this returns an error, return that error." |
| auto parsed_headers = ConvertCBORValueToHeaders(cbor_http_request_value); |
| if (!parsed_headers) { |
| RunErrorCallbackAndDestroy( |
| "Cannot parse headers in the index section."); |
| return; |
| } |
| |
| // Step 5.2. "If pseudos does not have keys named ':method' and |
| // ':url', or its size isn't 2, return an error." |
| const auto pseudo_method = parsed_headers->pseudos.find(":method"); |
| const auto pseudo_url = parsed_headers->pseudos.find(":url"); |
| if (parsed_headers->pseudos.size() != 2 || |
| pseudo_method == parsed_headers->pseudos.end() || |
| pseudo_url == parsed_headers->pseudos.end()) { |
| RunErrorCallbackAndDestroy( |
| "Request headers map must have exactly two pseudo-headers, :method " |
| "and :url."); |
| return; |
| } |
| |
| // Step 5.3. "If pseudos[':method'] is not 'GET', return an error." |
| if (pseudo_method->second != "GET") { |
| RunErrorCallbackAndDestroy("Request method must be GET."); |
| return; |
| } |
| |
| // Step 5.4. "Let parsedUrl be the result of parsing ([URL]) |
| // pseudos[':url'] with no base URL." |
| GURL parsed_url(pseudo_url->second); |
| |
| // Step 5.5. "If parsedUrl is a failure, its fragment is not null, or it |
| // includes credentials, return an error." |
| if (!parsed_url.is_valid() || parsed_url.has_ref() || |
| parsed_url.has_username() || parsed_url.has_password()) { |
| RunErrorCallbackAndDestroy( |
| ":url in header map must be a valid URL without fragment or " |
| "credentials."); |
| return; |
| } |
| |
| // Step 5.6. "Let http-request be a new request ([FETCH]) whose: |
| // - method is pseudos[':method'], |
| // - url is parsedUrl, |
| // - header list is headers, and |
| // - client is null." |
| mojom::BundleIndexItemPtr item = mojom::BundleIndexItem::New(); |
| item->request_method = pseudo_method->second; |
| item->request_url = std::move(parsed_url); |
| item->request_headers = std::move(parsed_headers->headers); |
| |
| // Step 5.7. "Let responseOffset be sectionOffsets["responses"].offset + |
| // currentOffset. This is relative to the start of the stream." |
| const uint64_t response_offset = |
| section_offsets_[kResponsesSection].first + current_offset; |
| |
| // Step 5.8. "If currentOffset + length is greater than |
| // sectionOffsets["responses"].length, return an error." |
| if (!length_value.is_unsigned()) { |
| RunErrorCallbackAndDestroy( |
| "Length value in the index section should be an unsigned."); |
| return; |
| } |
| const uint64_t length = length_value.GetUnsigned(); |
| |
| uint64_t response_end; |
| if (!base::CheckAdd(current_offset, length) |
| .AssignIfValid(&response_end) || |
| response_end > section_offsets_[kResponsesSection].second) { |
| RunErrorCallbackAndDestroy( |
| "Index map is invalid: total length of responses exceeds the " |
| "length of the responses section."); |
| return; |
| } |
| |
| // Step 5.9. "If requests[http-request] exists, return an error. That is, |
| // duplicate requests are forbidden." |
| |
| // TODO(crbug.com/966753): Implement this check. |
| |
| // Step 5.10. "Set requests[http-request] to a struct whose "offset" |
| // item is responseOffset and whose "length" item is length." |
| item->response_offset = response_offset; |
| item->response_length = length; |
| items_.push_back(std::move(item)); |
| |
| // Step 5.11. "Set currentOffset to currentOffset + length." |
| current_offset = response_end; |
| } |
| |
| // We're done. |
| RunSuccessCallbackAndDestroy( |
| mojom::BundleMetadata::New(std::move(items_), manifest_url_)); |
| } |
| |
| void RunSuccessCallbackAndDestroy(mojom::BundleMetadataPtr metadata) { |
| std::move(callback_).Run(std::move(metadata), nullptr); |
| delete this; |
| } |
| |
| void RunErrorCallbackAndDestroy(const std::string& message) { |
| std::move(callback_).Run(nullptr, |
| mojom::BundleMetadataParseError::New(message)); |
| delete this; |
| } |
| |
| // Implements SharedBundleDataSource::Observer. |
| void OnDisconnect() override { |
| RunErrorCallbackAndDestroy("Data source disconnected."); |
| } |
| |
| scoped_refptr<SharedBundleDataSource> data_source_; |
| ParseMetadataCallback callback_; |
| uint64_t size_; |
| // name -> (offset, length) |
| std::map<std::string, std::pair<uint64_t, uint64_t>> section_offsets_; |
| std::vector<mojom::BundleIndexItemPtr> items_; |
| GURL manifest_url_; |
| |
| base::WeakPtrFactory<MetadataParser> weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(MetadataParser); |
| }; |
| |
| // A parser for reading single item from the responses section. This class owns |
| // itself and will self destruct after calling the ParseResponseCallback. |
| class BundledExchangesParser::ResponseParser |
| : public BundledExchangesParser::SharedBundleDataSource::Observer { |
| public: |
| ResponseParser(scoped_refptr<SharedBundleDataSource> data_source, |
| uint64_t response_offset, |
| uint64_t response_length, |
| BundledExchangesParser::ParseResponseCallback callback) |
| : data_source_(data_source), |
| response_offset_(response_offset), |
| response_length_(response_length), |
| callback_(std::move(callback)) { |
| data_source_->AddObserver(this); |
| } |
| ~ResponseParser() override { data_source_->RemoveObserver(this); } |
| |
| void Start(uint64_t buffer_size = kInitialBufferSizeForResponse) { |
| const uint64_t length = std::min(response_length_, buffer_size); |
| data_source_->Read(response_offset_, length, |
| base::BindOnce(&ResponseParser::ParseResponseHeader, |
| weak_factory_.GetWeakPtr(), length)); |
| } |
| |
| private: |
| // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#load-response |
| void ParseResponseHeader(uint64_t expected_data_length, |
| const base::Optional<std::vector<uint8_t>>& data) { |
| // Step 1. "Seek to offset requestMetadata.offset in stream. If this fails, |
| // return an error." |
| if (!data || data->size() != expected_data_length) { |
| RunErrorCallbackAndDestroy("Error reading response header."); |
| return; |
| } |
| InputReader input(*data); |
| |
| // Step 2. "Read 1 byte from stream. If this is an error or isn't 0x82, |
| // return an error." |
| auto num_elements = input.ReadCBORHeader(CBORType::kArray); |
| if (!num_elements || *num_elements != 2) { |
| RunErrorCallbackAndDestroy("Array size of response must be 2."); |
| return; |
| } |
| |
| // Step 3. "Let headerLength be the result of getting the length of a CBOR |
| // bytestring header from stream (Section 3.5.2). If headerLength is an |
| // error, return that error." |
| auto header_length = input.ReadCBORHeader(CBORType::kByteString); |
| if (!header_length) { |
| RunErrorCallbackAndDestroy("Cannot parse response header length."); |
| return; |
| } |
| |
| // Step 4. "If headerLength is 524288 (512*1024) or greater, return an |
| // error." |
| if (*header_length >= kMaxResponseHeaderLength) { |
| RunErrorCallbackAndDestroy("Response header is too big."); |
| return; |
| } |
| |
| // If we don't have enough data for the headers and the CBOR header of the |
| // payload, re-read with a larger buffer size. |
| const uint64_t required_buffer_size = std::min( |
| input.CurrentOffset() + *header_length + kMaxCBORItemHeaderSize, |
| response_length_); |
| if (data->size() < required_buffer_size) { |
| DVLOG(1) << "Re-reading response header with a buffer of size " |
| << required_buffer_size; |
| Start(required_buffer_size); |
| return; |
| } |
| |
| // Step 5. "Let headerCbor be the result of reading headerLength bytes from |
| // stream and parsing a CBOR item from them matching the headers CDDL rule. |
| // If either the read or parse returns an error, return that error." |
| auto headers_bytes = input.ReadBytes(*header_length); |
| if (!headers_bytes) { |
| RunErrorCallbackAndDestroy("Cannot read response headers."); |
| return; |
| } |
| cbor::Reader::DecoderError error; |
| base::Optional<cbor::Value> headers_value = |
| cbor::Reader::Read(*headers_bytes, &error); |
| if (!headers_value) { |
| RunErrorCallbackAndDestroy("Cannot parse response headers."); |
| return; |
| } |
| |
| // Step 6. "Let (headers, pseudos) be the result of converting headerCbor |
| // to a header list and pseudoheaders using the algorithm in Section 3.6. |
| // If this returns an error, return that error." |
| auto parsed_headers = ConvertCBORValueToHeaders(*headers_value); |
| if (!parsed_headers) { |
| RunErrorCallbackAndDestroy("Cannot parse response headers."); |
| return; |
| } |
| |
| // Step 7. "If pseudos does not have a key named ':status' or its size |
| // isn't 1, return an error." |
| const auto pseudo_status = parsed_headers->pseudos.find(":status"); |
| if (parsed_headers->pseudos.size() != 1 || |
| pseudo_status == parsed_headers->pseudos.end()) { |
| RunErrorCallbackAndDestroy( |
| "Response headers map must have exactly one pseudo-header, :status."); |
| return; |
| } |
| |
| // Step 8. "If pseudos[':status'] isn't exactly 3 ASCII decimal digits, |
| // return an error." |
| int status; |
| const auto& status_str = pseudo_status->second; |
| if (status_str.size() != 3 || |
| !std::all_of(status_str.begin(), status_str.end(), |
| base::IsAsciiDigit<char>) || |
| !base::StringToInt(status_str, &status)) { |
| RunErrorCallbackAndDestroy(":status must be 3 ASCII decimal digits."); |
| return; |
| } |
| |
| // Step 9. "If headers does not contain a Content-Type header, return an |
| // error." |
| |
| // TODO(crbug.com/966753): Implement this once |
| // https://github.com/WICG/webpackage/issues/445 is resolved. |
| |
| // Step 10. "Let payloadLength be the result of getting the length of a CBOR |
| // bytestring header from stream (Section 3.5.2). If payloadLength is an |
| // error, return that error." |
| auto payload_length = input.ReadCBORHeader(CBORType::kByteString); |
| if (!payload_length) { |
| RunErrorCallbackAndDestroy("Cannot parse response payload length."); |
| return; |
| } |
| |
| // Step 11. "If stream.currentOffset + payloadLength != |
| // requestMetadata.offset + requestMetadata.length, return an error." |
| if (input.CurrentOffset() + *payload_length != response_length_) { |
| RunErrorCallbackAndDestroy("Unexpected payload length."); |
| return; |
| } |
| |
| // Step 12. "Let body be a new body ([FETCH]) whose stream is a tee'd copy |
| // of stream starting at the current offset and ending after payloadLength |
| // bytes." |
| |
| // Step 13. "Let response be a new response ([FETCH]) whose: |
| // - Url list is request's url list, |
| // - status is pseudos[':status'], |
| // - header list is headers, and |
| // - body is body." |
| mojom::BundleResponsePtr response = mojom::BundleResponse::New(); |
| response->response_code = status; |
| response->response_headers = std::move(parsed_headers->headers); |
| response->payload_offset = response_offset_ + input.CurrentOffset(); |
| response->payload_length = *payload_length; |
| RunSuccessCallbackAndDestroy(std::move(response)); |
| } |
| |
| void RunSuccessCallbackAndDestroy(mojom::BundleResponsePtr response) { |
| std::move(callback_).Run(std::move(response), nullptr); |
| delete this; |
| } |
| |
| void RunErrorCallbackAndDestroy(const std::string& message) { |
| std::move(callback_).Run(nullptr, |
| mojom::BundleResponseParseError::New(message)); |
| delete this; |
| } |
| |
| // Implements SharedBundleDataSource::Observer. |
| void OnDisconnect() override { |
| RunErrorCallbackAndDestroy("Data source disconnected."); |
| } |
| |
| scoped_refptr<SharedBundleDataSource> data_source_; |
| uint64_t response_offset_; |
| uint64_t response_length_; |
| ParseResponseCallback callback_; |
| |
| base::WeakPtrFactory<ResponseParser> weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(ResponseParser); |
| }; |
| |
| BundledExchangesParser::SharedBundleDataSource::SharedBundleDataSource( |
| mojo::PendingRemote<mojom::BundleDataSource> pending_data_source) |
| : data_source_(std::move(pending_data_source)) { |
| data_source_.set_disconnect_handler(base::BindOnce( |
| &SharedBundleDataSource::OnDisconnect, base::Unretained(this))); |
| } |
| |
| void BundledExchangesParser::SharedBundleDataSource::AddObserver( |
| Observer* observer) { |
| DCHECK(observers_.end() == observers_.find(observer)); |
| observers_.insert(observer); |
| } |
| |
| void BundledExchangesParser::SharedBundleDataSource::RemoveObserver( |
| Observer* observer) { |
| auto it = observers_.find(observer); |
| DCHECK(observers_.end() != it); |
| observers_.erase(it); |
| } |
| |
| BundledExchangesParser::SharedBundleDataSource::~SharedBundleDataSource() = |
| default; |
| |
| void BundledExchangesParser::SharedBundleDataSource::OnDisconnect() { |
| for (auto* observer : observers_) |
| observer->OnDisconnect(); |
| } |
| |
| void BundledExchangesParser::SharedBundleDataSource::GetSize( |
| GetSizeCallback callback) { |
| data_source_->GetSize(std::move(callback)); |
| } |
| |
| void BundledExchangesParser::SharedBundleDataSource::Read( |
| uint64_t offset, |
| uint64_t length, |
| ReadCallback callback) { |
| data_source_->Read(offset, length, std::move(callback)); |
| } |
| |
| BundledExchangesParser::BundledExchangesParser( |
| mojo::PendingReceiver<mojom::BundledExchangesParser> receiver, |
| mojo::PendingRemote<mojom::BundleDataSource> data_source) |
| : receiver_(this, std::move(receiver)), |
| data_source_(base::MakeRefCounted<SharedBundleDataSource>( |
| std::move(data_source))) { |
| receiver_.set_disconnect_handler(base::BindOnce( |
| &base::DeletePointer<BundledExchangesParser>, base::Unretained(this))); |
| } |
| |
| BundledExchangesParser::~BundledExchangesParser() = default; |
| |
| void BundledExchangesParser::ParseMetadata(ParseMetadataCallback callback) { |
| MetadataParser* parser = |
| new MetadataParser(data_source_, std::move(callback)); |
| parser->Start(); |
| } |
| |
| void BundledExchangesParser::ParseResponse( |
| uint64_t response_offset, |
| uint64_t response_length, |
| ParseResponseCallback callback) { |
| ResponseParser* parser = new ResponseParser( |
| data_source_, response_offset, response_length, std::move(callback)); |
| parser->Start(); |
| } |
| |
| } // namespace data_decoder |