Clean up Malformed Track definition per MOQT draft-14.

Due to the new subgroup encoding, those streams cannot have declining object IDs.

PiperOrigin-RevId: 804981345
diff --git a/quiche/quic/moqt/moqt_session.cc b/quiche/quic/moqt/moqt_session.cc
index 7005f76..1e73ae9 100644
--- a/quiche/quic/moqt/moqt_session.cc
+++ b/quiche/quic/moqt/moqt_session.cc
@@ -1729,10 +1729,6 @@
       session_->OnMalformedTrack(track);
       return;
     }
-    if (message.object_id < next_object_id_) {
-      session_->OnMalformedTrack(track);
-      return;
-    }
     if (end_of_message) {
       next_object_id_ = message.object_id + 1;
       if (message.object_status == MoqtObjectStatus::kEndOfTrack ||
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index 6d57458..967bc0a 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -3709,37 +3709,6 @@
   EXPECT_EQ(MoqtSessionPeer::remote_track(&session_, 0), nullptr);
 }
 
-TEST_F(MoqtSessionTest, SubgroupStreamOutOfOrder) {
-  MockSubscribeRemoteTrackVisitor remote_track_visitor;
-  MoqtSessionPeer::CreateRemoteTrack(&session_, DefaultSubscribe(),
-                                     /*track_alias=*/2, &remote_track_visitor);
-  webtransport::test::MockStream control_stream;
-  std::unique_ptr<MoqtControlParserVisitor> stream_input =
-      MoqtSessionPeer::CreateControlStream(&session_, &control_stream);
-  std::unique_ptr<MoqtDataParserVisitor> object_stream =
-      MoqtSessionPeer::CreateIncomingDataStream(
-          &session_, &mock_stream_,
-          MoqtDataStreamType::Subgroup(/*subgroup_id=*/0, /*first_object_id=*/1,
-                                       /*no_extension_headers=*/true));
-  object_stream->OnObjectMessage(
-      MoqtObject(/*track_alias=*/2, /*group_id=*/0, /*object_id=*/1,
-                 /*publisher_priority=*/0x80, /*extension_headers=*/"",
-                 MoqtObjectStatus::kNormal, /*subgroup_id=*/0,
-                 /*payload_length=*/3),
-      "foo", true);
-  EXPECT_CALL(mock_session_, GetStreamById(_))
-      .WillRepeatedly(Return(&control_stream));
-  EXPECT_CALL(control_stream,
-              Writev(ControlMessageOfType(MoqtMessageType::kUnsubscribe), _));
-  EXPECT_CALL(remote_track_visitor, OnMalformedTrack);
-  object_stream->OnObjectMessage(
-      MoqtObject(/*track_alias=*/2, /*group_id=*/0, /*object_id=*/1,
-                 /*publisher_priority=*/0x80, /*extension_headers=*/"",
-                 MoqtObjectStatus::kNormal, /*subgroup_id=*/0,
-                 /*payload_length=*/3),
-      "bar", true);
-}
-
 TEST_F(MoqtSessionTest, SubgroupStreamObjectAfterGroupEnd) {
   MockSubscribeRemoteTrackVisitor remote_track_visitor;
   MoqtSessionPeer::CreateRemoteTrack(&session_, DefaultSubscribe(),
diff --git a/quiche/quic/moqt/moqt_track.cc b/quiche/quic/moqt/moqt_track.cc
index dcb26f0..b61db08 100644
--- a/quiche/quic/moqt/moqt_track.cc
+++ b/quiche/quic/moqt/moqt_track.cc
@@ -169,11 +169,20 @@
 
 bool UpstreamFetch::LocationIsValid(Location location, MoqtObjectStatus status,
                                     bool end_of_message) {
-  if (no_more_objects_) {
-    return false;
+  if (end_of_track_.has_value()) {
+    // Cannot exceed or change end_of_track_.
+    if (location > end_of_track_) {
+      return false;
+    }
+    if (status == MoqtObjectStatus::kEndOfTrack && location != *end_of_track_) {
+      return false;
+    }
   }
   if (end_of_message && status == MoqtObjectStatus::kEndOfTrack) {
-    no_more_objects_ = true;
+    if (highest_location_.has_value() && location < *highest_location_) {
+      return false;
+    }
+    end_of_track_ = location;
   }
   bool last_group_is_finished = last_group_is_finished_;
   last_group_is_finished_ =
@@ -181,6 +190,11 @@
   std::optional<Location> last_location = last_location_;
   if (end_of_message) {
     last_location_ = location;
+    if (!highest_location_.has_value()) {
+      highest_location_ = location;
+    } else {
+      highest_location_ = std::max(*highest_location_, location);
+    }
   }
   if (!last_location.has_value()) {
     return true;
diff --git a/quiche/quic/moqt/moqt_track.h b/quiche/quic/moqt/moqt_track.h
index eeb3412..3568de6 100644
--- a/quiche/quic/moqt/moqt_track.h
+++ b/quiche/quic/moqt/moqt_track.h
@@ -350,9 +350,12 @@
 
  private:
   std::optional<MoqtDeliveryOrder> group_order_;  // nullopt if not yet known.
+  // The last object received on the stream.
   std::optional<Location> last_location_;
+  // The highest location received on the stream.
+  std::optional<Location> highest_location_;
   bool last_group_is_finished_ = false;  // Received EndOfGroup.
-  bool no_more_objects_ = false;         // Received EndOfTrack
+  std::optional<Location> end_of_track_;  // Received EndOfTrack
 
   quiche::QuicheWeakPtr<UpstreamFetchTask> task_;
 
diff --git a/quiche/quic/moqt/moqt_track_test.cc b/quiche/quic/moqt/moqt_track_test.cc
index 8ed99f9..4d8901d 100644
--- a/quiche/quic/moqt/moqt_track_test.cc
+++ b/quiche/quic/moqt/moqt_track_test.cc
@@ -281,6 +281,22 @@
       fetch_.LocationIsValid(Location(2, 1), MoqtObjectStatus::kNormal, true));
 }
 
+TEST_F(UpstreamFetchTest, LocationIsValidTwoEndsOfTrack) {
+  EXPECT_TRUE(fetch_.LocationIsValid(Location(1, 1),
+                                     MoqtObjectStatus::kEndOfTrack, true));
+  EXPECT_FALSE(fetch_.LocationIsValid(Location(1, 2),
+                                      MoqtObjectStatus::kEndOfTrack, true));
+}
+
+TEST_F(UpstreamFetchTest, LocationIsValidEndOfTrackTooLow) {
+  EXPECT_TRUE(
+      fetch_.LocationIsValid(Location(1, 2), MoqtObjectStatus::kNormal, true));
+  EXPECT_TRUE(
+      fetch_.LocationIsValid(Location(3, 0), MoqtObjectStatus::kNormal, true));
+  EXPECT_FALSE(fetch_.LocationIsValid(Location(2, 1),
+                                      MoqtObjectStatus::kEndOfTrack, true));
+}
+
 }  // namespace test
 
 }  // namespace moqt