Fix disallow_lone_cr_in_chunk_extension behavior.

Reuse `last_char_was_slash_r_` member that was previously only used in
BalsaFrame::ProcessHeaders().
PiperOrigin-RevId: 645032390
diff --git a/quiche/balsa/balsa_frame.cc b/quiche/balsa/balsa_frame.cc
index 1e6080f..1272e10 100644
--- a/quiche/balsa/balsa_frame.cc
+++ b/quiche/balsa/balsa_frame.cc
@@ -1111,6 +1111,7 @@
 
         --current;
         parse_state_ = BalsaFrameEnums::READING_CHUNK_EXTENSION;
+        last_char_was_slash_r_ = false;
         visitor_->OnChunkLength(chunk_length_remaining_);
         continue;
 
@@ -1129,11 +1130,22 @@
             return current - input;
           }
           const char c = *current;
-          if (http_validation_policy_.disallow_lone_cr_in_chunk_extension &&
-              c == '\r' && (current + 1 == end || *(current + 1) != '\n')) {
-            // We have a lone carriage return.
-            HandleError(BalsaFrameEnums::INVALID_CHUNK_EXTENSION);
-            return current - input;
+          if (http_validation_policy_.disallow_lone_cr_in_chunk_extension) {
+            // This is a CR character and the next one is not LF.
+            const bool cr_followed_by_non_lf =
+                c == '\r' && current + 1 < end && *(current + 1) != '\n';
+            // The last character processed by the last ProcessInput() call was
+            // CR, this is the first character of the current ProcessInput()
+            // call, and it is not LF.
+            const bool previous_cr_followed_by_non_lf =
+                last_char_was_slash_r_ && current == input && c != '\n';
+            if (cr_followed_by_non_lf || previous_cr_followed_by_non_lf) {
+              HandleError(BalsaFrameEnums::INVALID_CHUNK_EXTENSION);
+              return current - input;
+            }
+            if (current + 1 == end) {
+              last_char_was_slash_r_ = c == '\r';
+            }
           }
           if (c == '\r' || c == '\n') {
             extensions_length = (extensions_start == current)
diff --git a/quiche/balsa/balsa_frame_test.cc b/quiche/balsa/balsa_frame_test.cc
index 75d041f..76c02ec 100644
--- a/quiche/balsa/balsa_frame_test.cc
+++ b/quiche/balsa/balsa_frame_test.cc
@@ -1728,9 +1728,10 @@
                             message.size());
 }
 
-// TODO(b/347710034): Fix behavior.
-TEST_F(HTTPBalsaFrameTest,
-       ChunkExtensionLoneCarriageReturnDetectionAtBoundary) {
+// Regression test for b/347710034: `disallow_lone_cr_in_chunk_extension` should
+// not trigger false positive when "\r\n" terminating chunk length is separated
+// into multiple calls to ProcessInput().
+TEST_F(HTTPBalsaFrameTest, ChunkExtensionCarriageReturnLineFeedAtBoundary) {
   balsa_frame_.set_http_validation_policy(
       HttpValidationPolicy{.disallow_lone_cr_in_chunk_extension = true});
   EXPECT_CALL(visitor_mock_, ProcessHeaders(_));
@@ -1742,7 +1743,43 @@
             balsa_frame_.ProcessInput(headers.data(), headers.size()));
 
   constexpr absl::string_view body1("3\r");
-  ASSERT_EQ(1, balsa_frame_.ProcessInput(body1.data(), body1.size()));
+  ASSERT_EQ(body1.size(),
+            balsa_frame_.ProcessInput(body1.data(), body1.size()));
+  ASSERT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode());
+
+  constexpr absl::string_view body2(
+      "\nfoo\r\n"
+      "0\r\n\r\n");
+
+  EXPECT_CALL(visitor_mock_, OnBodyChunkInput("foo"));
+  EXPECT_CALL(visitor_mock_, MessageDone());
+  ASSERT_EQ(body2.size(),
+            balsa_frame_.ProcessInput(body2.data(), body2.size()));
+
+  EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode());
+  EXPECT_TRUE(balsa_frame_.MessageFullyRead());
+}
+
+// A CR character followed by a non-LF character is detected even if separated
+// into multiple calls to ProcessInput().
+TEST_F(HTTPBalsaFrameTest, ChunkExtensionLoneCarriageReturnAtBoundary) {
+  balsa_frame_.set_http_validation_policy(
+      HttpValidationPolicy{.disallow_lone_cr_in_chunk_extension = true});
+  EXPECT_CALL(visitor_mock_, ProcessHeaders(_));
+  EXPECT_CALL(visitor_mock_, HeaderDone());
+  constexpr absl::string_view headers(
+      "POST / HTTP/1.1\r\n"
+      "transfer-encoding: chunked\r\n\r\n");
+  ASSERT_EQ(headers.size(),
+            balsa_frame_.ProcessInput(headers.data(), headers.size()));
+
+  constexpr absl::string_view body1("3\r");
+  ASSERT_EQ(body1.size(),
+            balsa_frame_.ProcessInput(body1.data(), body1.size()));
+  ASSERT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode());
+
+  constexpr absl::string_view body2("a");
+  EXPECT_EQ(0, balsa_frame_.ProcessInput(body2.data(), body2.size()));
   EXPECT_EQ(BalsaFrameEnums::INVALID_CHUNK_EXTENSION, balsa_frame_.ErrorCode());
 }