Platformize AsciiUrlDecode

This function broke Chromium merge because of a different method signature for DecodeURLEscapeSequences. This CL allows us to override the method in Chromium. We've confirmed that this works in Chromium. This CL also fixes a bug where URI expansion was incorrectly leaving unknown variables instead of removing them. That issue was found by use the Chromium platform override in the corresponding merge CL.

PiperOrigin-RevId: 406948688
diff --git a/common/platform/api/quiche_url_utils.h b/common/platform/api/quiche_url_utils.h
index 126c17a..e6c9fc9 100644
--- a/common/platform/api/quiche_url_utils.h
+++ b/common/platform/api/quiche_url_utils.h
@@ -9,6 +9,8 @@
 
 #include "absl/container/flat_hash_map.h"
 #include "absl/container/flat_hash_set.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "quiche_platform_impl/quiche_url_utils_impl.h"
 
 namespace quiche {
@@ -25,6 +27,12 @@
   return ExpandURITemplateImpl(uri_template, parameters, target, vars_found);
 }
 
+// Decodes a URL-encoded string and converts it to ASCII. If the decoded input
+// contains non-ASCII characters, decoding fails and absl::nullopt is returned.
+inline absl::optional<std::string> AsciiUrlDecode(absl::string_view input) {
+  return AsciiUrlDecodeImpl(input);
+}
+
 }  // namespace quiche
 
 #endif  // QUICHE_COMMON_PLATFORM_API_QUICHE_URL_UTILS_H_
diff --git a/common/platform/api/quiche_url_utils_test.cc b/common/platform/api/quiche_url_utils_test.cc
index e99fb9d..1a59d74 100644
--- a/common/platform/api/quiche_url_utils_test.cc
+++ b/common/platform/api/quiche_url_utils_test.cc
@@ -9,6 +9,7 @@
 
 #include "absl/container/flat_hash_map.h"
 #include "absl/container/flat_hash_set.h"
+#include "absl/types/optional.h"
 #include "common/platform/api/quiche_test.h"
 
 namespace quiche {
@@ -34,12 +35,12 @@
 
 TEST(QuicheUrlUtilsTest, ExtraParameter) {
   ValidateExpansion("/{foo}/{bar}/{baz}/", {{"foo", "123"}, {"bar", "456"}},
-                    "/123/456/{baz}/", {"foo", "bar"});
+                    "/123/456//", {"foo", "bar"});
 }
 
 TEST(QuicheUrlUtilsTest, MissingParameter) {
-  ValidateExpansion("/{foo}/{baz}/", {{"foo", "123"}, {"bar", "456"}},
-                    "/123/{baz}/", {"foo"});
+  ValidateExpansion("/{foo}/{baz}/", {{"foo", "123"}, {"bar", "456"}}, "/123//",
+                    {"foo"});
 }
 
 TEST(QuicheUrlUtilsTest, RepeatedParameter) {
@@ -52,5 +53,28 @@
                     "/123/%3A/", {"foo", "bar"});
 }
 
+void ValidateUrlDecode(const std::string& input,
+                       const absl::optional<std::string>& expected_output) {
+  absl::optional<std::string> decode_result = AsciiUrlDecode(input);
+  if (!expected_output.has_value()) {
+    EXPECT_FALSE(decode_result.has_value());
+    return;
+  }
+  ASSERT_TRUE(decode_result.has_value());
+  EXPECT_EQ(decode_result.value(), expected_output);
+}
+
+TEST(QuicheUrlUtilsTest, DecodeNoChange) {
+  ValidateUrlDecode("foobar", "foobar");
+}
+
+TEST(QuicheUrlUtilsTest, DecodeReplace) {
+  ValidateUrlDecode("%7Bfoobar%7D", "{foobar}");
+}
+
+TEST(QuicheUrlUtilsTest, DecodeFail) {
+  ValidateUrlDecode("%FF", absl::nullopt);
+}
+
 }  // namespace
 }  // namespace quiche
diff --git a/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.cc b/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.cc
index 801d95d..bcdfa9b 100644
--- a/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.cc
+++ b/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.cc
@@ -4,10 +4,18 @@
 
 #include "quiche_platform_impl/quiche_url_utils_impl.h"
 
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <string>
+
 #include "absl/container/flat_hash_map.h"
 #include "absl/container/flat_hash_set.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_replace.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "url/url_canon.h"
 #include "url/url_util.h"
 
 namespace quiche {
@@ -31,6 +39,18 @@
       found.insert(name);
     }
   }
+  // Remove any remaining variables that were not present in |parameters|.
+  while (true) {
+    size_t start = result.find('{');
+    if (start == std::string::npos) {
+      break;
+    }
+    size_t end = result.find('}');
+    if (end == std::string::npos || end <= start) {
+      return false;
+    }
+    result.erase(start, (end - start) + 1);
+  }
   if (vars_found != nullptr) {
     *vars_found = found;
   }
@@ -38,4 +58,21 @@
   return true;
 }
 
+absl::optional<std::string> AsciiUrlDecodeImpl(absl::string_view input) {
+  std::string input_encoded = std::string(input);
+  url::RawCanonOutputW<1024> canon_output;
+  url::DecodeURLEscapeSequences(input_encoded.c_str(), input_encoded.length(),
+                                &canon_output);
+  std::string output;
+  output.reserve(canon_output.length());
+  for (int i = 0; i < canon_output.length(); i++) {
+    const uint16_t c = reinterpret_cast<uint16_t*>(canon_output.data())[i];
+    if (c > std::numeric_limits<signed char>::max()) {
+      return absl::nullopt;
+    }
+    output += static_cast<char>(c);
+  }
+  return output;
+}
+
 }  // namespace quiche
diff --git a/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.h b/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.h
index 45f0e42..3bcf3b0 100644
--- a/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.h
+++ b/common/platform/default/quiche_platform_impl/quiche_url_utils_impl.h
@@ -9,6 +9,9 @@
 
 #include "absl/container/flat_hash_map.h"
 #include "absl/container/flat_hash_set.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "common/platform/api/quiche_export.h"
 
 namespace quiche {
 
@@ -16,12 +19,17 @@
 // Parameters are URL-encoded. Collects the names of any expanded variables in
 // |vars_found|. Supports level 1 templates as specified in RFC 6570. Returns
 // true if the template was parseable, false if it was malformed.
-bool ExpandURITemplateImpl(
+QUICHE_EXPORT_PRIVATE bool ExpandURITemplateImpl(
     const std::string& uri_template,
     const absl::flat_hash_map<std::string, std::string>& parameters,
     std::string* target,
     absl::flat_hash_set<std::string>* vars_found = nullptr);
 
+// Decodes a URL-encoded string and converts it to ASCII. If the decoded input
+// contains non-ASCII characters, decoding fails and absl::nullopt is returned.
+QUICHE_EXPORT_PRIVATE absl::optional<std::string> AsciiUrlDecodeImpl(
+    absl::string_view input);
+
 }  // namespace quiche
 
 #endif  // QUICHE_COMMON_PLATFORM_DEFAULT_QUICHE_PLATFORM_IMPL_QUICHE_URL_UTILS_IMPL_H_
diff --git a/quic/masque/masque_server_session.cc b/quic/masque/masque_server_session.cc
index b3aa99e..6484d8a 100644
--- a/quic/masque/masque_server_session.cc
+++ b/quic/masque/masque_server_session.cc
@@ -10,13 +10,14 @@
 #include <limits>
 
 #include "absl/strings/str_cat.h"
+#include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/optional.h"
-#include "url/url_util.h"
 #include "quic/core/http/spdy_utils.h"
 #include "quic/core/quic_data_reader.h"
 #include "quic/core/quic_udp_socket.h"
 #include "quic/tools/quic_url.h"
+#include "common/platform/api/quiche_url_utils.h"
 
 namespace quic {
 
@@ -72,23 +73,6 @@
   return response;
 }
 
-absl::optional<std::string> AsciiUrlDecode(absl::string_view input) {
-  std::string input_encoded = std::string(input);
-  url::RawCanonOutputW<1024> canon_output;
-  DecodeURLEscapeSequences(input_encoded.c_str(), input_encoded.length(),
-                           &canon_output);
-  std::string output;
-  output.reserve(canon_output.length());
-  for (int i = 0; i < canon_output.length(); i++) {
-    const uint16_t c = reinterpret_cast<uint16_t*>(canon_output.data())[i];
-    if (c > std::numeric_limits<signed char>::max()) {
-      return absl::nullopt;
-    }
-    output += static_cast<char>(c);
-  }
-  return output;
-}
-
 }  // namespace
 
 MasqueServerSession::MasqueServerSession(
@@ -251,12 +235,12 @@
       QUIC_DLOG(ERROR) << "MASQUE request with bad path \"" << path << "\"";
       return CreateBackendErrorResponse("400", "Bad path");
     }
-    absl::optional<std::string> host = AsciiUrlDecode(path_split[1]);
+    absl::optional<std::string> host = quiche::AsciiUrlDecode(path_split[1]);
     if (!host.has_value()) {
       QUIC_DLOG(ERROR) << "Failed to decode host \"" << path_split[1] << "\"";
       return CreateBackendErrorResponse("500", "Failed to decode host");
     }
-    absl::optional<std::string> port = AsciiUrlDecode(path_split[2]);
+    absl::optional<std::string> port = quiche::AsciiUrlDecode(path_split[2]);
     if (!port.has_value()) {
       QUIC_DLOG(ERROR) << "Failed to decode port \"" << path_split[2] << "\"";
       return CreateBackendErrorResponse("500", "Failed to decode port");