| // Copyright 2014 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #include "components/omnibox/browser/search_suggestion_parser.h" | 
 |  | 
 | #include <stddef.h> | 
 |  | 
 | #include <algorithm> | 
 | #include <memory> | 
 | #include <optional> | 
 | #include <string_view> | 
 |  | 
 | #include "base/base64.h" | 
 | #include "base/check.h" | 
 | #include "base/containers/contains.h" | 
 | #include "base/containers/fixed_flat_map.h" | 
 | #include "base/feature_list.h" | 
 | #include "base/i18n/icu_string_conversions.h" | 
 | #include "base/json/json_reader.h" | 
 | #include "base/json/json_writer.h" | 
 | #include "base/metrics/histogram_functions.h" | 
 | #include "base/strings/string_number_conversions.h" | 
 | #include "base/strings/string_split.h" | 
 | #include "base/strings/string_util.h" | 
 | #include "base/strings/utf_string_conversions.h" | 
 | #include "base/values.h" | 
 | #include "components/omnibox/browser/autocomplete_i18n.h" | 
 | #include "components/omnibox/browser/autocomplete_input.h" | 
 | #include "components/omnibox/browser/autocomplete_match_classification.h" | 
 | #include "components/omnibox/browser/autocomplete_provider.h" | 
 | #include "components/omnibox/browser/omnibox_field_trial.h" | 
 | #include "components/omnibox/browser/suggestion_group_util.h" | 
 | #include "components/omnibox/browser/url_prefix.h" | 
 | #include "components/omnibox/common/omnibox_feature_configs.h" | 
 | #include "components/omnibox/common/omnibox_features.h" | 
 | #include "components/strings/grit/components_strings.h" | 
 | #include "components/url_formatter/url_fixer.h" | 
 | #include "components/url_formatter/url_formatter.h" | 
 | #include "net/http/http_response_headers.h" | 
 | #include "services/network/public/cpp/simple_url_loader.h" | 
 | #include "services/network/public/mojom/url_response_head.mojom.h" | 
 | #include "third_party/omnibox_proto/entity_info.pb.h" | 
 | #include "third_party/omnibox_proto/navigational_intent.pb.h" | 
 | #include "third_party/omnibox_proto/rich_suggest_template.pb.h" | 
 | #include "third_party/omnibox_proto/suggest_template_info.pb.h" | 
 | #include "ui/base/device_form_factor.h" | 
 | #include "ui/base/l10n/l10n_util.h" | 
 | #include "url/url_constants.h" | 
 |  | 
 | namespace { | 
 |  | 
 | // Converts a suggestion type name found in the JSON response to an equivalent | 
 | // omnibox::SuggestType enum value. | 
 | omnibox::SuggestType GetSuggestType(const std::string& type) { | 
 |   if (type == "CALCULATOR") { | 
 |     return omnibox::TYPE_CALCULATOR; | 
 |   } | 
 |   if (type == "ENTITY") { | 
 |     return omnibox::TYPE_ENTITY; | 
 |   } | 
 |   if (type == "TAIL") { | 
 |     return omnibox::TYPE_TAIL; | 
 |   } | 
 |   if (type == "PERSONALIZED_QUERY") { | 
 |     return omnibox::TYPE_PERSONALIZED_QUERY; | 
 |   } | 
 |   if (type == "PROFILE") { | 
 |     return omnibox::TYPE_PROFILE; | 
 |   } | 
 |   if (type == "NAVIGATION") { | 
 |     return omnibox::TYPE_NAVIGATION; | 
 |   } | 
 |   if (type == "PERSONALIZED_NAVIGATION") { | 
 |     return omnibox::TYPE_PERSONALIZED_NAVIGATION; | 
 |   } | 
 |   if (type == "CHROME_QUERY_TILES") { | 
 |     return omnibox::TYPE_CHROME_QUERY_TILES; | 
 |   } | 
 |   if (type == "CATEGORICAL_QUERY") { | 
 |     return omnibox::TYPE_CATEGORICAL_QUERY; | 
 |   } | 
 |   return omnibox::TYPE_QUERY; | 
 | } | 
 |  | 
 | // Converts an omnibox::SuggestType enum value to an equivalent | 
 | // AutocompleteMatchType::Type enum values. | 
 | AutocompleteMatchType::Type GetAutocompleteMatchType( | 
 |     omnibox::SuggestType suggest_type) { | 
 |   switch (suggest_type) { | 
 |     case omnibox::TYPE_CALCULATOR: | 
 |       return AutocompleteMatchType::CALCULATOR; | 
 |     case omnibox::TYPE_ENTITY: | 
 |       return AutocompleteMatchType::SEARCH_SUGGEST_ENTITY; | 
 |     case omnibox::TYPE_TAIL: | 
 |       return AutocompleteMatchType::SEARCH_SUGGEST_TAIL; | 
 |     case omnibox::TYPE_PERSONALIZED_QUERY: | 
 |       return AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED; | 
 |     case omnibox::TYPE_PROFILE: | 
 |       return AutocompleteMatchType::SEARCH_SUGGEST_PROFILE; | 
 |     case omnibox::TYPE_NAVIGATION: | 
 |       return AutocompleteMatchType::NAVSUGGEST; | 
 |     case omnibox::TYPE_PERSONALIZED_NAVIGATION: | 
 |       return AutocompleteMatchType::NAVSUGGEST_PERSONALIZED; | 
 |     default: { | 
 |       // Use `ACMatchType::SEARCH_SUGGEST_ENTITY` for categorical suggestions. | 
 |       if (suggest_type == omnibox::TYPE_CATEGORICAL_QUERY && | 
 |           base::FeatureList::IsEnabled(omnibox::kCategoricalSuggestions)) { | 
 |         return AutocompleteMatchType::SEARCH_SUGGEST_ENTITY; | 
 |       } | 
 |       return AutocompleteMatchType::SEARCH_SUGGEST; | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | // Convert the supplied Json::Value representation of list-of-lists-of-integers | 
 | // to a vector-of-vecrors-of-integers, containing (ideally) one vector of | 
 | // integers per match. | 
 | // The logic here does not validate if the length of top level vector is same as | 
 | // number of returned matches and will supply empty vector for any item that is | 
 | // either invalid or missing. | 
 | // The function will always return a valid and properly sized vector of vectors, | 
 | // equal in length to `expected_size`, even if the input `subtypes_list` is not | 
 | // valid. | 
 | std::vector<std::vector<int>> ParseMatchSubtypes( | 
 |     const base::Value::List* subtypes_list, | 
 |     size_t expected_size) { | 
 |   std::vector<std::vector<int>> result(expected_size); | 
 |  | 
 |   if (subtypes_list == nullptr) { | 
 |     return result; | 
 |   } | 
 |  | 
 |   if (!subtypes_list->empty() && subtypes_list->size() != expected_size) { | 
 |     LOG(WARNING) << "The length of reported subtypes (" << subtypes_list->size() | 
 |                  << ") does not match the expected length (" << expected_size | 
 |                  << ')'; | 
 |   } | 
 |  | 
 |   const auto num_items = std::min(expected_size, subtypes_list->size()); | 
 |   for (auto index = 0u; index < num_items; index++) { | 
 |     const auto& subtypes_item = (*subtypes_list)[index]; | 
 |     // Permissive: ignore subtypes that are not in a form of a list. | 
 |     if (!subtypes_item.is_list()) | 
 |       continue; | 
 |  | 
 |     const auto& subtype_list = subtypes_item.GetList(); | 
 |     auto& result_subtypes = result[index]; | 
 |     result_subtypes.reserve(subtype_list.size()); | 
 |  | 
 |     for (const auto& subtype : subtype_list) { | 
 |       // Permissive: Skip over any item that is not an integer. | 
 |       if (!subtype.is_int()) | 
 |         continue; | 
 |       result_subtypes.emplace_back(subtype.GetInt()); | 
 |     } | 
 |   } | 
 |  | 
 |   return result; | 
 | } | 
 |  | 
 | std::string FindStringOrEmpty(const base::Value::Dict& value, std::string key) { | 
 |   auto* ptr = value.FindString(key); | 
 |   return ptr ? *ptr : ""; | 
 | } | 
 |  | 
 | // The field number for the experiment stat type specified as an int | 
 | // in ExperimentStatsV2. | 
 | constexpr char kTypeIntFieldNumber[] = "4"; | 
 | // The field number for the string value in ExperimentStatsV2. | 
 | constexpr char kStringValueFieldNumber[] = "2"; | 
 |  | 
 | constexpr auto kReservedReservedGroupSectionsMap = | 
 |     base::MakeFixedFlatMap<int, omnibox::GroupSection>( | 
 |         {{0, omnibox::SECTION_REMOTE_ZPS_1}, | 
 |          {1, omnibox::SECTION_REMOTE_ZPS_2}, | 
 |          {2, omnibox::SECTION_REMOTE_ZPS_3}, | 
 |          {3, omnibox::SECTION_REMOTE_ZPS_4}, | 
 |          {4, omnibox::SECTION_REMOTE_ZPS_5}, | 
 |          {5, omnibox::SECTION_REMOTE_ZPS_6}, | 
 |          {6, omnibox::SECTION_REMOTE_ZPS_7}, | 
 |          {7, omnibox::SECTION_REMOTE_ZPS_8}, | 
 |          {8, omnibox::SECTION_REMOTE_ZPS_9}, | 
 |          {9, omnibox::SECTION_REMOTE_ZPS_10}}); | 
 |  | 
 | // Converts the given 0-based index of a group in the server response to a group | 
 | // section known to Chrome. | 
 | omnibox::GroupSection ChromeGroupSectionForRemoteGroupIndex( | 
 |     const int group_index) { | 
 |   if (base::Contains(kReservedReservedGroupSectionsMap, group_index)) { | 
 |     return kReservedReservedGroupSectionsMap.at(group_index); | 
 |   } else { | 
 |     // Return a default section if we don't have any reserved sections left. | 
 |     return omnibox::SECTION_DEFAULT; | 
 |   } | 
 | } | 
 |  | 
 | // Decodes a proto object from its serialized Base64 string representation. | 
 | template <typename T> | 
 | bool DecodeProtoFromBase64(const std::string* encoded_data, T& result_proto) { | 
 |   if (!encoded_data || encoded_data->empty()) { | 
 |     return false; | 
 |   } | 
 |  | 
 |   std::string decoded_data; | 
 |   if (!base::Base64Decode(*encoded_data, &decoded_data)) { | 
 |     return false; | 
 |   } | 
 |  | 
 |   if (decoded_data.empty()) { | 
 |     return false; | 
 |   } | 
 |  | 
 |   if (!result_proto.ParseFromString(decoded_data)) { | 
 |     return false; | 
 |   } | 
 |  | 
 |   return true; | 
 | } | 
 |  | 
 | // Format template image URLs that do not contain a scheme. | 
 | // The call to GetFormattedURL() will return the URL with a scheme added or | 
 | // return the same URL if no formatting is necessary. | 
 | void FormatAnswerTemplateImageURL( | 
 |     omnibox::RichAnswerTemplate* answer_template) { | 
 |   if (!(answer_template->answers_size() > 0)) { | 
 |     return; | 
 |   } | 
 |   std::string* url_string = | 
 |       answer_template->mutable_answers(0)->mutable_image()->mutable_url(); | 
 |   answer_template->mutable_answers(0)->mutable_image()->set_url( | 
 |       omnibox::answer_data_parser::GetFormattedURL(url_string).spec()); | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | omnibox::SuggestSubtype SuggestSubtypeForNumber(int value) { | 
 |   // Note that ideally this should first check if `value` is valid by calling | 
 |   // omnibox::SuggestSubtype_IsValid and return omnibox::SUBTYPE_NONE when there | 
 |   // is no corresponding enum object. However, that is not possible because the | 
 |   // current list of subtypes in omnibox::SuggestSubtype is not exhaustive. | 
 |   // However, casting int values into omnibox::SuggestSubtype without testing | 
 |   // membership is expected to be safe as omnibox::SuggestSubtype has a fixed | 
 |   // int underlying type. | 
 |   return static_cast<omnibox::SuggestSubtype>(value); | 
 | } | 
 |  | 
 | omnibox::NavigationalIntent NavigationalIntentForNumber(int value) { | 
 |   if (omnibox::NavigationalIntent_IsValid(value)) { | 
 |     return static_cast<omnibox::NavigationalIntent>(value); | 
 |   } | 
 |   return omnibox::NavigationalIntent::NAV_INTENT_NONE; | 
 | } | 
 |  | 
 | omnibox::AnswerType AnswerTypeForNumber(int value) { | 
 |   if (omnibox::AnswerType_IsValid(value)) { | 
 |     return static_cast<omnibox::AnswerType>(value); | 
 |   } | 
 |   return omnibox::ANSWER_TYPE_UNSPECIFIED; | 
 | } | 
 |  | 
 | // SearchSuggestionParser::Result ---------------------------------------------- | 
 |  | 
 | SearchSuggestionParser::Result::Result( | 
 |     bool from_keyword, | 
 |     int relevance, | 
 |     bool relevance_from_server, | 
 |     AutocompleteMatchType::Type type, | 
 |     omnibox::SuggestType suggest_type, | 
 |     std::vector<int> subtypes, | 
 |     const std::string& deletion_url, | 
 |     omnibox::NavigationalIntent navigational_intent) | 
 |     : from_keyword_(from_keyword), | 
 |       type_(type), | 
 |       suggest_type_(suggest_type), | 
 |       subtypes_(std::move(subtypes)), | 
 |       relevance_(relevance), | 
 |       relevance_from_server_(relevance_from_server), | 
 |       received_after_last_keystroke_(true), | 
 |       deletion_url_(deletion_url), | 
 |       navigational_intent_(navigational_intent) {} | 
 |  | 
 | SearchSuggestionParser::Result::Result(const Result& other) = default; | 
 |  | 
 | SearchSuggestionParser::Result::~Result() = default; | 
 |  | 
 | // SearchSuggestionParser::SuggestResult --------------------------------------- | 
 |  | 
 | SearchSuggestionParser::SuggestResult::SuggestResult( | 
 |     const std::u16string& suggestion, | 
 |     AutocompleteMatchType::Type type, | 
 |     omnibox::SuggestType suggest_type, | 
 |     std::vector<int> subtypes, | 
 |     bool from_keyword, | 
 |     omnibox::NavigationalIntent navigational_intent, | 
 |     int relevance, | 
 |     bool relevance_from_server, | 
 |     const std::u16string& input_text) | 
 |     : SuggestResult(suggestion, | 
 |                     type, | 
 |                     suggest_type, | 
 |                     std::move(subtypes), | 
 |                     suggestion, | 
 |                     /*match_contents_prefix=*/std::u16string(), | 
 |                     /*annotation=*/std::u16string(), | 
 |                     /*entity_info=*/omnibox::EntityInfo(), | 
 |                     /*deletion_url=*/"", | 
 |                     from_keyword, | 
 |                     navigational_intent, | 
 |                     relevance, | 
 |                     relevance_from_server, | 
 |                     /*should_prefetch=*/false, | 
 |                     /*should_prerender=*/false, | 
 |                     input_text) {} | 
 |  | 
 | SearchSuggestionParser::SuggestResult::SuggestResult( | 
 |     const std::u16string& suggestion, | 
 |     AutocompleteMatchType::Type type, | 
 |     omnibox::SuggestType suggest_type, | 
 |     std::vector<int> subtypes, | 
 |     const std::u16string& match_contents, | 
 |     const std::u16string& match_contents_prefix, | 
 |     const std::u16string& annotation, | 
 |     omnibox::EntityInfo entity_info, | 
 |     const std::string& deletion_url, | 
 |     bool from_keyword, | 
 |     omnibox::NavigationalIntent navigational_intent, | 
 |     int relevance, | 
 |     bool relevance_from_server, | 
 |     bool should_prefetch, | 
 |     bool should_prerender, | 
 |     const std::u16string& input_text) | 
 |     : Result(from_keyword, | 
 |              relevance, | 
 |              relevance_from_server, | 
 |              type, | 
 |              suggest_type, | 
 |              std::move(subtypes), | 
 |              deletion_url, | 
 |              navigational_intent), | 
 |       suggestion_(suggestion), | 
 |       match_contents_prefix_(match_contents_prefix), | 
 |       entity_info_(std::move(entity_info)), | 
 |       should_prefetch_(should_prefetch), | 
 |       should_prerender_(should_prerender) { | 
 |   annotation_ = !entity_info_.annotation().empty() | 
 |                     ? base::UTF8ToUTF16(entity_info_.annotation()) | 
 |                     : annotation; | 
 |   match_contents_ = !entity_info_.name().empty() | 
 |                         ? base::UTF8ToUTF16(entity_info_.name()) | 
 |                         : match_contents; | 
 |   match_contents_ = base::CollapseWhitespace(match_contents_, false); | 
 |   DCHECK(!match_contents_.empty()); | 
 |   ClassifyMatchContents(true, input_text); | 
 | } | 
 |  | 
 | SearchSuggestionParser::SuggestResult::SuggestResult( | 
 |     const SuggestResult& result) = default; | 
 |  | 
 | SearchSuggestionParser::SuggestResult::~SuggestResult() = default; | 
 |  | 
 | SearchSuggestionParser::SuggestResult& | 
 | SearchSuggestionParser::SuggestResult::operator=(const SuggestResult& rhs) = | 
 |     default; | 
 |  | 
 | void SearchSuggestionParser::SuggestResult::ClassifyMatchContents( | 
 |     const bool allow_bolding_all, | 
 |     const std::u16string& input_text) { | 
 |   DCHECK(!match_contents_.empty()); | 
 |  | 
 |   // In case of zero-suggest results, do not highlight matches. | 
 |   if (input_text.empty()) { | 
 |     match_contents_class_ = { | 
 |         ACMatchClassification(0, ACMatchClassification::NONE)}; | 
 |     return; | 
 |   } | 
 |  | 
 |   std::u16string lookup_text = input_text; | 
 |   if (type_ == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) { | 
 |     const size_t contents_index = | 
 |         suggestion_.length() - match_contents_.length(); | 
 |     // Ensure the query starts with the input text, and ends with the match | 
 |     // contents, and the input text has an overlap with contents. | 
 |     if (base::StartsWith(suggestion_, input_text, | 
 |                          base::CompareCase::SENSITIVE) && | 
 |         base::EndsWith(suggestion_, match_contents_, | 
 |                        base::CompareCase::SENSITIVE) && | 
 |         (input_text.length() > contents_index)) { | 
 |       lookup_text = input_text.substr(contents_index); | 
 |     } | 
 |   } | 
 |   // Do a case-insensitive search for |lookup_text|. | 
 |   auto lookup_result = std::ranges::search(match_contents_, lookup_text, | 
 |                                            SimpleCaseInsensitiveCompareUCS2()); | 
 |   if (!allow_bolding_all && lookup_result.empty()) { | 
 |     // Bail if the code below to update the bolding would bold the whole | 
 |     // string.  Note that the string may already be entirely bolded; if | 
 |     // so, leave it as is. | 
 |     return; | 
 |   } | 
 |  | 
 |   // Note we discard our existing match_contents_class_ with this call. | 
 |   match_contents_class_ = | 
 |       ClassifyAllMatchesInString(input_text, match_contents_, true); | 
 | } | 
 |  | 
 | void SearchSuggestionParser::SuggestResult::SetRichAnswerTemplate( | 
 |     const omnibox::RichAnswerTemplate& answer_template) { | 
 |   answer_template_ = answer_template; | 
 | } | 
 |  | 
 | void SearchSuggestionParser::SuggestResult::SetAnswerType( | 
 |     const omnibox::AnswerType& answer_type) { | 
 |   answer_type_ = answer_type; | 
 | } | 
 |  | 
 | void SearchSuggestionParser::SuggestResult::SetEntityInfo( | 
 |     const omnibox::EntityInfo& entity_info) { | 
 |   entity_info_ = entity_info; | 
 | } | 
 |  | 
 | void SearchSuggestionParser::SuggestResult::SetSuggestTemplateInfo( | 
 |     const omnibox::SuggestTemplateInfo& suggest_template_info) { | 
 |   suggest_template_info_ = suggest_template_info; | 
 | } | 
 |  | 
 | void SearchSuggestionParser::SuggestResult::SetMatchContents( | 
 |     const std::u16string& match_contents) { | 
 |   match_contents_ = match_contents; | 
 | } | 
 |  | 
 | void SearchSuggestionParser::SuggestResult::SetAnnotation( | 
 |     const std::u16string& annotation) { | 
 |   annotation_ = annotation; | 
 | } | 
 |  | 
 | int SearchSuggestionParser::SuggestResult::CalculateRelevance( | 
 |     const AutocompleteInput& input, | 
 |     bool keyword_provider_requested) const { | 
 |   if (!from_keyword_ && keyword_provider_requested) | 
 |     return 100; | 
 |   return ((input.type() == metrics::OmniboxInputType::URL) ? 300 : 600); | 
 | } | 
 |  | 
 | // SearchSuggestionParser::NavigationResult ------------------------------------ | 
 |  | 
 | SearchSuggestionParser::NavigationResult::NavigationResult( | 
 |     const AutocompleteSchemeClassifier& scheme_classifier, | 
 |     const GURL& url, | 
 |     AutocompleteMatchType::Type match_type, | 
 |     omnibox::SuggestType suggest_type, | 
 |     std::vector<int> subtypes, | 
 |     const std::u16string& description, | 
 |     const std::string& deletion_url, | 
 |     bool from_keyword, | 
 |     omnibox::NavigationalIntent navigational_intent, | 
 |     int relevance, | 
 |     bool relevance_from_server, | 
 |     const std::u16string& input_text) | 
 |     : Result(from_keyword, | 
 |              relevance, | 
 |              relevance_from_server, | 
 |              match_type, | 
 |              suggest_type, | 
 |              std::move(subtypes), | 
 |              deletion_url, | 
 |              navigational_intent), | 
 |       url_(url), | 
 |       formatted_url_(AutocompleteInput::FormattedStringWithEquivalentMeaning( | 
 |           url, | 
 |           url_formatter::FormatUrl(url, | 
 |                                    url_formatter::kFormatUrlOmitDefaults & | 
 |                                        ~url_formatter::kFormatUrlOmitHTTP, | 
 |                                    base::UnescapeRule::SPACES, | 
 |                                    nullptr, | 
 |                                    nullptr, | 
 |                                    nullptr), | 
 |           scheme_classifier, | 
 |           nullptr)), | 
 |       description_(description) { | 
 |   DCHECK(url_.is_valid()); | 
 |   CalculateAndClassifyMatchContents(true, input_text); | 
 |   ClassifyDescription(input_text); | 
 | } | 
 |  | 
 | SearchSuggestionParser::NavigationResult::NavigationResult( | 
 |     const NavigationResult& other) = default; | 
 |  | 
 | SearchSuggestionParser::NavigationResult::~NavigationResult() = default; | 
 |  | 
 | void SearchSuggestionParser::NavigationResult:: | 
 |     CalculateAndClassifyMatchContents(const bool allow_bolding_nothing, | 
 |                                       const std::u16string& input_text) { | 
 |   // Start with the trivial nothing-bolded classification. | 
 |   DCHECK(url_.is_valid()); | 
 |  | 
 |   // In case of zero-suggest results, do not highlight matches. | 
 |   if (input_text.empty()) { | 
 |     // TODO(tommycli): Maybe this should actually return | 
 |     // ACMatchClassification::URL. I'm not changing this now because this CL | 
 |     // is meant to fix a regression only, but we should consider this for | 
 |     // consistency with other |input_text| that matches nothing. | 
 |     match_contents_class_ = { | 
 |         ACMatchClassification(0, ACMatchClassification::NONE)}; | 
 |     return; | 
 |   } | 
 |  | 
 |   // Set contents to the formatted URL while ensuring the scheme and subdomain | 
 |   // are kept if the user text seems to include them. E.g., for the user text | 
 |   // 'http google.com', the contents should not trim 'http'. | 
 |   bool match_in_scheme = false; | 
 |   bool match_in_subdomain = false; | 
 |   TermMatches term_matches_in_url = FindTermMatches(input_text, formatted_url_); | 
 |   // Convert TermMatches (offset, length) to MatchPosition (start, end). | 
 |   std::vector<AutocompleteMatch::MatchPosition> match_positions; | 
 |   for (auto match : term_matches_in_url) | 
 |     match_positions.emplace_back(match.offset, match.offset + match.length); | 
 |   AutocompleteMatch::GetMatchComponents(GURL(formatted_url_), match_positions, | 
 |                                         &match_in_scheme, &match_in_subdomain); | 
 |   auto format_types = AutocompleteMatch::GetFormatTypes( | 
 |       GURL(input_text).has_scheme(), match_in_subdomain); | 
 |  | 
 |   // Find matches in the potentially new match_contents | 
 |   std::u16string match_contents = | 
 |       url_formatter::FormatUrl(url_, format_types, base::UnescapeRule::SPACES, | 
 |                                nullptr, nullptr, nullptr); | 
 |   TermMatches term_matches = FindTermMatches(input_text, match_contents); | 
 |  | 
 |   // Update |match_contents_| and |match_contents_class_| if it's allowed. | 
 |   if (allow_bolding_nothing || !term_matches.empty()) { | 
 |     match_contents_ = match_contents; | 
 |     match_contents_class_ = ClassifyTermMatches( | 
 |         term_matches, match_contents.size(), | 
 |         ACMatchClassification::MATCH | ACMatchClassification::URL, | 
 |         ACMatchClassification::URL); | 
 |   } | 
 | } | 
 |  | 
 | int SearchSuggestionParser::NavigationResult::CalculateRelevance( | 
 |     const AutocompleteInput& input, | 
 |     bool keyword_provider_requested) const { | 
 |   return (from_keyword_ || !keyword_provider_requested) ? 800 : 150; | 
 | } | 
 |  | 
 | void SearchSuggestionParser::NavigationResult::ClassifyDescription( | 
 |     const std::u16string& input_text) { | 
 |   TermMatches term_matches = FindTermMatches(input_text, description_); | 
 |   description_class_ = ClassifyTermMatches(term_matches, description_.size(), | 
 |                                            ACMatchClassification::MATCH, | 
 |                                            ACMatchClassification::NONE); | 
 | } | 
 |  | 
 | // SearchSuggestionParser::Results --------------------------------------------- | 
 |  | 
 | SearchSuggestionParser::Results::Results() | 
 |     : verbatim_relevance(-1), | 
 |       field_trial_triggered(false), | 
 |       relevances_from_server(false) {} | 
 |  | 
 | SearchSuggestionParser::Results::~Results() = default; | 
 |  | 
 | void SearchSuggestionParser::Results::Clear() { | 
 |   suggest_results.clear(); | 
 |   navigation_results.clear(); | 
 |   verbatim_relevance = -1; | 
 |   metadata.clear(); | 
 |   field_trial_triggered = false; | 
 |   experiment_stats_v2s.clear(); | 
 |   relevances_from_server = false; | 
 |   suggestion_groups_map.clear(); | 
 | } | 
 |  | 
 | bool SearchSuggestionParser::Results::HasServerProvidedScores() const { | 
 |   if (verbatim_relevance >= 0) | 
 |     return true; | 
 |  | 
 |   // Right now either all results of one type will be server-scored or they will | 
 |   // all be locally scored, but in case we change this later, we'll just check | 
 |   // them all. | 
 |   for (auto i(suggest_results.begin()); i != suggest_results.end(); ++i) { | 
 |     if (i->relevance_from_server()) | 
 |       return true; | 
 |   } | 
 |   for (auto i(navigation_results.begin()); i != navigation_results.end(); ++i) { | 
 |     if (i->relevance_from_server()) | 
 |       return true; | 
 |   } | 
 |  | 
 |   return false; | 
 | } | 
 |  | 
 | // SearchSuggestionParser ------------------------------------------------------ | 
 |  | 
 | // static | 
 | std::string SearchSuggestionParser::ExtractJsonData( | 
 |     const network::SimpleURLLoader* source, | 
 |     std::unique_ptr<std::string> response_body) { | 
 |   const net::HttpResponseHeaders* response_headers = nullptr; | 
 |   if (source && source->ResponseInfo()) | 
 |     response_headers = source->ResponseInfo()->headers.get(); | 
 |   if (!response_body) | 
 |     return std::string(); | 
 |  | 
 |   std::string json_data = std::move(*response_body); | 
 |  | 
 |   // JSON is supposed to be UTF-8, but some suggest service providers send | 
 |   // JSON files in non-UTF-8 encodings.  The actual encoding is usually | 
 |   // specified in the Content-Type header field. | 
 |   if (response_headers) { | 
 |     std::string charset; | 
 |     if (response_headers->GetCharset(&charset)) { | 
 |       std::u16string data_16; | 
 |       // TODO(jungshik): Switch to CodePageToUTF8 after it's added. | 
 |       if (base::CodepageToUTF16(json_data, charset.c_str(), | 
 |                                 base::OnStringConversionError::FAIL, &data_16)) | 
 |         json_data = base::UTF16ToUTF8(data_16); | 
 |     } | 
 |   } | 
 |   return json_data; | 
 | } | 
 |  | 
 | // static | 
 | std::optional<base::Value::List> SearchSuggestionParser::DeserializeJsonData( | 
 |     std::string_view json_data) { | 
 |   // The JSON response should be an array. | 
 |   for (size_t response_start_index = json_data.find("["), i = 0; | 
 |        response_start_index != std::string_view::npos && i < 5; | 
 |        response_start_index = json_data.find("[", 1), i++) { | 
 |     // Remove any XSSI guards to allow for JSON parsing. | 
 |     json_data.remove_prefix(response_start_index); | 
 |  | 
 |     std::optional<base::Value> data = | 
 |         base::JSONReader::Read(json_data, base::JSON_ALLOW_TRAILING_COMMAS); | 
 |     if (data && data->is_list()) { | 
 |       return std::move(data->GetList()); | 
 |     } | 
 |   } | 
 |   return std::nullopt; | 
 | } | 
 |  | 
 | // static | 
 | bool SearchSuggestionParser::ParseSuggestResults( | 
 |     const base::Value::List& root_list, | 
 |     const AutocompleteInput& input, | 
 |     const AutocompleteSchemeClassifier& scheme_classifier, | 
 |     int default_result_relevance, | 
 |     bool is_keyword_result, | 
 |     Results* results) { | 
 |   const std::u16string input_text = input.IsZeroSuggest() ? u"" : input.text(); | 
 |  | 
 |   // 1st element: query. | 
 |   if (root_list.empty() || !root_list[0].is_string()) | 
 |     return false; | 
 |   std::u16string query = base::UTF8ToUTF16(root_list[0].GetString()); | 
 |   if (query != input_text) { | 
 |     return false; | 
 |   } | 
 |  | 
 |   // 2nd element: suggestions list. | 
 |   if (root_list.size() < 2u || !root_list[1].is_list()) | 
 |     return false; | 
 |   const auto& results_list = root_list[1].GetList(); | 
 |  | 
 |   // 3rd element: Ignore the optional description list for now. | 
 |   // 4th element: Disregard the query URL list. | 
 |   // 5th element: Disregard the optional key-value pairs from the server. | 
 |  | 
 |   // Reset suggested relevance information. | 
 |   results->verbatim_relevance = -1; | 
 |  | 
 |   const base::Value::List* suggest_types = nullptr; | 
 |   const base::Value::List* suggest_subtypes = nullptr; | 
 |   const base::Value::List* nav_intents = nullptr; | 
 |   const base::Value::List* relevances = nullptr; | 
 |   const base::Value::List* suggestion_details = nullptr; | 
 |   const base::Value::List* subtype_identifiers = nullptr; | 
 |   int prefetch_index = -1; | 
 |   int prerender_index = -1; | 
 |   omnibox::GroupsInfo groups_info; | 
 |  | 
 |   if (root_list.size() > 4u && root_list[4].is_dict()) { | 
 |     const base::Value::Dict& extras = root_list[4].GetDict(); | 
 |  | 
 |     suggest_types = extras.FindList("google:suggesttype"); | 
 |  | 
 |     suggest_subtypes = extras.FindList("google:suggestsubtypes"); | 
 |  | 
 |     nav_intents = extras.FindList("google:suggestnavintents"); | 
 |  | 
 |     relevances = extras.FindList("google:suggestrelevance"); | 
 |     // Discard this list if its size does not match that of the suggestions. | 
 |     if (relevances && relevances->size() != results_list.size()) { | 
 |       relevances = nullptr; | 
 |     } | 
 |  | 
 |     if (std::optional<int> relevance = | 
 |             extras.FindInt("google:verbatimrelevance")) { | 
 |       results->verbatim_relevance = *relevance; | 
 |     } | 
 |  | 
 |     if (const std::string* gws_event_id_hash_str = | 
 |             extras.FindString("google:suggesteventid")) { | 
 |       int64_t gws_event_id_hash; | 
 |       if (base::StringToInt64(*gws_event_id_hash_str, &gws_event_id_hash)) { | 
 |         results->gws_event_id_hashes.push_back(gws_event_id_hash); | 
 |       } | 
 |     } | 
 |  | 
 |     // Check if the active suggest field trial (if any) has triggered either | 
 |     // for the default provider or keyword provider. | 
 |     std::optional<bool> field_trial_triggered = | 
 |         extras.FindBool("google:fieldtrialtriggered"); | 
 |     results->field_trial_triggered = field_trial_triggered.value_or(false); | 
 |  | 
 |     results->experiment_stats_v2s.clear(); | 
 |     const base::Value::List* experiment_stats_v2s_list = | 
 |         extras.FindList("google:experimentstats"); | 
 |     if (experiment_stats_v2s_list) { | 
 |       for (const auto& experiment_stats_v2_value : *experiment_stats_v2s_list) { | 
 |         const base::Value::Dict* experiment_stats_v2_dict = | 
 |             experiment_stats_v2_value.GetIfDict(); | 
 |         if (!experiment_stats_v2_dict) { | 
 |           continue; | 
 |         } | 
 |         std::optional<int> type_int = | 
 |             experiment_stats_v2_dict->FindInt(kTypeIntFieldNumber); | 
 |         const auto* string_value = | 
 |             experiment_stats_v2_dict->FindString(kStringValueFieldNumber); | 
 |         if (!type_int || !string_value) { | 
 |           continue; | 
 |         } | 
 |         omnibox::metrics::ChromeSearchboxStats::ExperimentStatsV2 | 
 |             experiment_stats_v2; | 
 |         experiment_stats_v2.set_type_int(*type_int); | 
 |         experiment_stats_v2.set_string_value(*string_value); | 
 |         results->experiment_stats_v2s.push_back(std::move(experiment_stats_v2)); | 
 |       } | 
 |     } | 
 |  | 
 |     const auto* groups_info_string = extras.FindString("google:groupsinfo"); | 
 |     DecodeProtoFromBase64<omnibox::GroupsInfo>(groups_info_string, groups_info); | 
 |  | 
 |     const base::Value::Dict* client_data = extras.FindDict("google:clientdata"); | 
 |     if (client_data) { | 
 |       prefetch_index = client_data->FindInt("phi").value_or(-1); | 
 |       prerender_index = client_data->FindInt("pre").value_or(-1); | 
 |     } | 
 |  | 
 |     suggestion_details = extras.FindList("google:suggestdetail"); | 
 |     // Discard this list if its size does not match that of the suggestions. | 
 |     if (suggestion_details && | 
 |         suggestion_details->size() != results_list.size()) { | 
 |       suggestion_details = nullptr; | 
 |     } | 
 |  | 
 |     // Legacy code: Get subtype identifiers. | 
 |     subtype_identifiers = extras.FindList("google:subtypeid"); | 
 |     // Discard this list if its size does not match that of the suggestions. | 
 |     if (subtype_identifiers && | 
 |         subtype_identifiers->size() != results_list.size()) { | 
 |       subtype_identifiers = nullptr; | 
 |     } | 
 |  | 
 |     // Store the metadata that came with the response in case we need to pass | 
 |     // it along with the prefetch query to Instant. | 
 |     base::JSONWriter::Write(extras, &results->metadata); | 
 |   } | 
 |  | 
 |   // Processed list of match subtypes, one vector per match. | 
 |   // Note: ParseMatchSubtypes will handle the cases where the key does not | 
 |   // exist or contains malformed data. | 
 |   std::vector<std::vector<int>> subtypes = | 
 |       ParseMatchSubtypes(suggest_subtypes, results_list.size()); | 
 |  | 
 |   // Clear the previous results now that new results are available. | 
 |   results->suggest_results.clear(); | 
 |   results->navigation_results.clear(); | 
 |  | 
 |   std::string type; | 
 |   int relevance = default_result_relevance; | 
 |   const std::u16string& trimmed_input = | 
 |       base::CollapseWhitespace(input_text, false); | 
 |  | 
 |   for (size_t index = 0; | 
 |        index < results_list.size() && results_list[index].is_string(); | 
 |        ++index) { | 
 |     std::u16string suggestion = | 
 |         base::UTF8ToUTF16(results_list[index].GetString()); | 
 |     // Google search may return empty suggestions for weird input characters, | 
 |     // they make no sense at all and can cause problems in our code. | 
 |     suggestion = base::CollapseWhitespace(suggestion, false); | 
 |     if (suggestion.empty()) | 
 |       continue; | 
 |  | 
 |     omnibox::NavigationalIntent nav_intent = omnibox::NAV_INTENT_NONE; | 
 |     if (nav_intents && index < nav_intents->size() && | 
 |         (*nav_intents)[index].is_int()) { | 
 |       nav_intent = NavigationalIntentForNumber((*nav_intents)[index].GetInt()); | 
 |     } | 
 |  | 
 |     // Apply valid suggested relevance scores; discard invalid lists. | 
 |     if (relevances) { | 
 |       if (!(*relevances)[index].is_int()) { | 
 |         relevances = nullptr; | 
 |       } else { | 
 |         relevance = (*relevances)[index].GetInt(); | 
 |       } | 
 |     } | 
 |  | 
 |     AutocompleteMatchType::Type match_type = | 
 |         AutocompleteMatchType::SEARCH_SUGGEST; | 
 |     omnibox::SuggestType suggest_type = omnibox::TYPE_QUERY; | 
 |  | 
 |     // Legacy code: if the server sends us a single subtype ID, place it beside | 
 |     // other subtypes. | 
 |     if (subtype_identifiers && index < subtype_identifiers->size() && | 
 |         (*subtype_identifiers)[index].is_int()) { | 
 |       subtypes[index].emplace_back((*subtype_identifiers)[index].GetInt()); | 
 |     } | 
 |  | 
 |     if (suggest_types && index < suggest_types->size() && | 
 |         (*suggest_types)[index].is_string()) { | 
 |       suggest_type = GetSuggestType((*suggest_types)[index].GetString()); | 
 |       match_type = GetAutocompleteMatchType(suggest_type); | 
 |     } | 
 |  | 
 |     std::string deletion_url; | 
 |     if (suggestion_details && index < suggestion_details->size() && | 
 |         (*suggestion_details)[index].is_dict()) { | 
 |       const base::Value::Dict& suggestion_detail = | 
 |           (*suggestion_details)[index].GetDict(); | 
 |       deletion_url = FindStringOrEmpty(suggestion_detail, "du"); | 
 |     } | 
 |  | 
 |     if ((match_type == AutocompleteMatchType::NAVSUGGEST) || | 
 |         (match_type == AutocompleteMatchType::NAVSUGGEST_PERSONALIZED)) { | 
 |       // Do not blindly trust the URL coming from the server to be valid. | 
 |       GURL url(url_formatter::FixupURL(base::UTF16ToUTF8(suggestion), | 
 |                                        std::string())); | 
 |       if (url.is_valid()) { | 
 |         std::u16string title; | 
 |         // 3rd element: optional descriptions list | 
 |         if (root_list.size() > 2u && root_list[2].is_list()) { | 
 |           const auto& descriptions = root_list[2].GetList(); | 
 |           if (index < descriptions.size() && descriptions[index].is_string()) { | 
 |             title = base::UTF8ToUTF16(descriptions[index].GetString()); | 
 |           } | 
 |         } | 
 |         results->navigation_results.push_back(NavigationResult( | 
 |             scheme_classifier, url, match_type, suggest_type, subtypes[index], | 
 |             title, deletion_url, is_keyword_result, nav_intent, relevance, | 
 |             relevances != nullptr, input_text)); | 
 |       } | 
 |     } else { | 
 |       std::u16string annotation; | 
 |       std::u16string match_contents = suggestion; | 
 |       if (match_type == AutocompleteMatchType::CALCULATOR) { | 
 |         const bool has_equals_prefix = !suggestion.compare(0, 2, u"= "); | 
 |         if (has_equals_prefix) { | 
 |           // Calculator results include a "= " prefix but we don't want to | 
 |           // include this in the search terms. | 
 |           suggestion.erase(0, 2); | 
 |           // Unlikely to happen, but better to be safe. | 
 |           if (base::CollapseWhitespace(suggestion, false).empty()) | 
 |             continue; | 
 |         } | 
 |         if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_DESKTOP) { | 
 |           match_contents = l10n_util::GetStringFUTF16( | 
 |               IDS_OMNIBOX_ONE_LINE_CALCULATOR_SUGGESTION_TEMPLATE, query, | 
 |               suggestion); | 
 |         } | 
 |       } | 
 |  | 
 |       omnibox::RichSuggestTemplate suggest_template; | 
 |       omnibox::EntityInfo entity_info; | 
 |       omnibox::SuggestTemplateInfo suggest_template_info; | 
 |       std::u16string match_contents_prefix; | 
 |       std::optional<int> suggestion_group_id; | 
 |       bool answer_parsed_successfully = false; | 
 |       omnibox::RichAnswerTemplate answer_template; | 
 |       omnibox::AnswerType answer_type = omnibox::ANSWER_TYPE_UNSPECIFIED; | 
 |       bool has_suggest_template = false; | 
 |  | 
 |       if (suggestion_details && (*suggestion_details)[index].is_dict() && | 
 |           !(*suggestion_details)[index].GetDict().empty()) { | 
 |         const base::Value::Dict& suggestion_detail = | 
 |             (*suggestion_details)[index].GetDict(); | 
 |  | 
 |         // Rich Suggest Template. | 
 |         const auto* rich_template_str = | 
 |             suggestion_detail.FindString("google:templateinfo"); | 
 |         DecodeProtoFromBase64<omnibox::RichSuggestTemplate>(rich_template_str, | 
 |                                                             suggest_template); | 
 |  | 
 |         // Entity. | 
 |         const auto* entity_info_string = | 
 |             suggestion_detail.FindString("google:entityinfo"); | 
 |         DecodeProtoFromBase64<omnibox::EntityInfo>(entity_info_string, | 
 |                                                    entity_info); | 
 |  | 
 |         // Suggest Template Info. | 
 |         const auto* suggest_info_string = | 
 |             suggestion_detail.FindString("google:suggesttemplate"); | 
 |         has_suggest_template = | 
 |             DecodeProtoFromBase64<omnibox::SuggestTemplateInfo>( | 
 |                 suggest_info_string, suggest_template_info); | 
 |  | 
 |         // Tail Suggest. | 
 |         std::string match_contents_tail = | 
 |             FindStringOrEmpty(suggestion_detail, "t"); | 
 |         if (!match_contents_tail.empty()) { | 
 |           match_contents = base::UTF8ToUTF16(match_contents_tail); | 
 |         } | 
 |         match_contents_prefix = | 
 |             base::UTF8ToUTF16(FindStringOrEmpty(suggestion_detail, "mp")); | 
 |  | 
 |         // Suggestion group Id. | 
 |         suggestion_group_id = suggestion_detail.FindInt("zl"); | 
 |  | 
 |         // Answer. | 
 |         const std::string* answer_type_str = | 
 |             suggestion_detail.FindString("ansb"); | 
 |         if (answer_type_str) { | 
 |           // Check that answer type string can be mapped to omnibox::AnswerType. | 
 |           int numeric_answer_type = 0; | 
 |           if (base::StringToInt(base::UTF8ToUTF16(*answer_type_str), | 
 |                                 &numeric_answer_type)) { | 
 |             base::UmaHistogramSparse("Omnibox.AnswerParseType", | 
 |                                      numeric_answer_type); | 
 |             answer_type = AnswerTypeForNumber(numeric_answer_type); | 
 |           } | 
 |         } | 
 |         if (answer_type != omnibox::ANSWER_TYPE_UNSPECIFIED) { | 
 |           // omnibox::RichAnswerTemplate is preferred to "ansa" if available. | 
 |           if (suggest_template.has_rich_answer_template() && | 
 |               !OmniboxFieldTrial::kAnswerActionsCounterfactual.Get()) { | 
 |             answer_template = suggest_template.rich_answer_template(); | 
 |             FormatAnswerTemplateImageURL(&answer_template); | 
 |             // Ensure `answer_template` has an answer. | 
 |             answer_parsed_successfully = answer_template.answers_size() > 0; | 
 |           } else if (const auto* answer_json = | 
 |                          suggestion_detail.FindDict("ansa")) { | 
 |               answer_parsed_successfully = | 
 |                   omnibox::answer_data_parser::ParseJsonToAnswerData( | 
 |                       *answer_json, &answer_template); | 
 |           } | 
 |           base::UmaHistogramBoolean("Omnibox.AnswerParseSuccess", | 
 |                                     answer_parsed_successfully); | 
 |         } | 
 |       } | 
 |  | 
 |       int int_index = static_cast<int>(index); | 
 |       bool should_prefetch = int_index == prefetch_index; | 
 |       bool should_prerender = int_index == prerender_index; | 
 |       results->suggest_results.push_back( | 
 |           SuggestResult(suggestion, match_type, suggest_type, subtypes[index], | 
 |                         match_contents, match_contents_prefix, annotation, | 
 |                         std::move(entity_info), deletion_url, is_keyword_result, | 
 |                         nav_intent, relevance, relevances != nullptr, | 
 |                         should_prefetch, should_prerender, trimmed_input)); | 
 |  | 
 |       if (answer_parsed_successfully) { | 
 |         // Ensure `answer_template` has an answer. | 
 |         DCHECK(answer_template.answers_size() > 0); | 
 |         results->suggest_results.back().SetAnswerType(answer_type); | 
 |         results->suggest_results.back().SetRichAnswerTemplate(answer_template); | 
 |       } | 
 |  | 
 |       // Update suggest result match contents and annotation to use | 
 |       // SuggestTemplateInfo if it is sent from server. | 
 |       if (has_suggest_template) { | 
 |         results->suggest_results.back().SetSuggestTemplateInfo( | 
 |             suggest_template_info); | 
 |         if (!suggest_template_info.primary_text().text().empty()) { | 
 |           results->suggest_results.back().SetMatchContents( | 
 |               base::UTF8ToUTF16(suggest_template_info.primary_text().text())); | 
 |         } | 
 |         if (!suggest_template_info.secondary_text().text().empty()) { | 
 |           results->suggest_results.back().SetAnnotation( | 
 |               base::UTF8ToUTF16(suggest_template_info.secondary_text().text())); | 
 |         } | 
 |       } | 
 |  | 
 |       if (suggestion_group_id) { | 
 |         results->suggest_results.back().set_suggestion_group_id( | 
 |             omnibox::GroupIdForNumber(*suggestion_group_id)); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   results->relevances_from_server = relevances != nullptr; | 
 |  | 
 |   // Keeps track of the position of the server-provided group IDs. | 
 |   size_t group_index = 0; | 
 |  | 
 |   // Adds the given group config to the results for the given group ID. Returns | 
 |   // true if the entry was added to or was already present in the results. | 
 |   auto add_group_config = [&](const omnibox::GroupId suggestion_group_id, | 
 |                               const omnibox::GroupConfig& group_config) { | 
 |     // Do not add the group config if the group ID is invalid or unknown to | 
 |     // Chrome. | 
 |     if (suggestion_group_id == omnibox::GROUP_INVALID) { | 
 |       return false; | 
 |     } | 
 |  | 
 |     // There is nothing to do if the group config has been added before. | 
 |     if (base::Contains(results->suggestion_groups_map, suggestion_group_id)) { | 
 |       return true; | 
 |     } | 
 |  | 
 |     // Store the group config with the appropriate section in the results. | 
 |     results->suggestion_groups_map[suggestion_group_id].MergeFrom(group_config); | 
 |     results->suggestion_groups_map[suggestion_group_id].set_section( | 
 |         ChromeGroupSectionForRemoteGroupIndex(group_index++)); | 
 |     return true; | 
 |   }; | 
 |  | 
 |   // Add the group configs associated with the suggestions. | 
 |   for (auto& suggest_result : results->suggest_results) { | 
 |     if (!suggest_result.suggestion_group_id().has_value()) { | 
 |       continue; | 
 |     } | 
 |  | 
 |     const omnibox::GroupId suggestion_group_id = | 
 |         suggest_result.suggestion_group_id().value(); | 
 |  | 
 |     // Add the group config associated with the suggestion, if the suggestion | 
 |     // has a valid group ID and a corresponding group config is found in the | 
 |     // response. | 
 |     if (!base::Contains(groups_info.group_configs(), suggestion_group_id) || | 
 |         !add_group_config(suggestion_group_id, groups_info.group_configs().at( | 
 |                                                    suggestion_group_id))) { | 
 |       continue; | 
 |     } | 
 |   } | 
 |  | 
 |   // Add the remaining group configs without any suggestions in the response. | 
 |   // The only known use case is the personalized zero-suggest which is also | 
 |   // produced by Chrome and relies on the server-provided group config to show | 
 |   // with the appropriate header text, where a header text is applicable. | 
 |   for (const auto& entry : groups_info.group_configs()) { | 
 |     add_group_config(omnibox::GroupIdForNumber(entry.first), entry.second); | 
 |   } | 
 |  | 
 |   return true; | 
 | } |