Adds `quiche::header_properties::HasInvalidPathChar()`.

Returns true if any character is not in the set allowed by RFC 3986 Sections 3.3 or 3.4.

Protected by new code, not yet used; not protected.

PiperOrigin-RevId: 662548538
diff --git a/quiche/balsa/header_properties.cc b/quiche/balsa/header_properties.cc
index 53978df..8d24172 100644
--- a/quiche/balsa/header_properties.cc
+++ b/quiche/balsa/header_properties.cc
@@ -86,6 +86,15 @@
   return invalidCharTable;
 }
 
+std::array<bool, 256> buildInvalidPathCharLookupTable() {
+  std::array<bool, 256> invalidCharTable;
+  invalidCharTable.fill(true);
+  for (uint8_t c : kValidPathCharList) {
+    invalidCharTable[c] = false;
+  }
+  return invalidCharTable;
+}
+
 }  // anonymous namespace
 
 bool IsMultivaluedHeader(absl::string_view header) {
@@ -124,4 +133,15 @@
   return false;
 }
 
+bool HasInvalidPathChar(absl::string_view value) {
+  static const std::array<bool, 256> invalidCharTable =
+      buildInvalidPathCharLookupTable();
+  for (const char c : value) {
+    if (invalidCharTable[c]) {
+      return true;
+    }
+  }
+  return false;
+}
+
 }  // namespace quiche::header_properties
diff --git a/quiche/balsa/header_properties.h b/quiche/balsa/header_properties.h
index 5a8cc24..7bee5f7 100644
--- a/quiche/balsa/header_properties.h
+++ b/quiche/balsa/header_properties.h
@@ -48,6 +48,12 @@
     0x0C, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
     0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x7F};
 
+// The set of characters allowed in the Path and Query components of a URI, as
+// described in RFC 3986 Sections 3.3 and 3.4.
+inline constexpr char kValidPathCharList[] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%!$&'()*"
+    "+,;=:@/?";
+
 // Returns true if the given `c` is invalid in a header field name. The first
 // version is spec compliant, the second one incorrectly allows '"'.
 QUICHE_EXPORT bool IsInvalidHeaderKeyChar(uint8_t c);
@@ -57,6 +63,10 @@
 QUICHE_EXPORT bool IsInvalidHeaderChar(uint8_t c);
 QUICHE_EXPORT bool HasInvalidHeaderChars(absl::string_view value);
 
+// Returns true if `value` contains a character not allowed in a path or query
+// component of a URI.
+QUICHE_EXPORT bool HasInvalidPathChar(absl::string_view value);
+
 }  // namespace quiche::header_properties
 
 #endif  // QUICHE_BALSA_HEADER_PROPERTIES_H_
diff --git a/quiche/balsa/header_properties_test.cc b/quiche/balsa/header_properties_test.cc
index 1930354..253c97c 100644
--- a/quiche/balsa/header_properties_test.cc
+++ b/quiche/balsa/header_properties_test.cc
@@ -100,5 +100,19 @@
   EXPECT_FALSE(HasInvalidHeaderChars("\x42 is a nice character"));
 }
 
+TEST(HeaderPropertiesTest, HasInvalidPathChar) {
+  EXPECT_FALSE(HasInvalidPathChar(""));
+  EXPECT_FALSE(HasInvalidPathChar("/"));
+  EXPECT_FALSE(HasInvalidPathChar("invalid_path/but/valid/chars"));
+  EXPECT_FALSE(HasInvalidPathChar("/path/with?query;fragment"));
+  EXPECT_FALSE(HasInvalidPathChar("/path2.fun/my_site-root/!&$=,+*()/wow"));
+
+  EXPECT_TRUE(HasInvalidPathChar("/path with spaces"));
+  EXPECT_TRUE(HasInvalidPathChar("/path\rwith\tother\nwhitespace"));
+  EXPECT_TRUE(HasInvalidPathChar("/square[brackets]not/allowed"));
+  EXPECT_TRUE(HasInvalidPathChar("/backtick`"));
+  EXPECT_TRUE(HasInvalidPathChar("/angle<brackets>also/bad"));
+}
+
 }  // namespace
 }  // namespace quiche::header_properties::test