This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new f440f7e78 fix(provider): avoid topic consumer pagination overflow 
(#2198)
f440f7e78 is described below

commit f440f7e78a2b2fd5e8446a5613cbdf9e0fdf69ac
Author: btlqql <[email protected]>
AuthorDate: Tue Aug 18 18:55:14 2026 +0800

    fix(provider): avoid topic consumer pagination overflow (#2198)
    
    Calculate pagination offsets with long arithmetic so very large validated 
page numbers return an empty page instead of wrapping to a negative index.
    
    Signed-off-by: btlqql <[email protected]>
---
 .../rocketmq/studio/common/util/Pagination.java    | 42 ++++++++++++++++
 .../rocketmq/studio/provider/InstanceProvider.java |  6 ++-
 .../studio/provider/apache/MetadataProvider.java   |  6 ++-
 .../provider/apache/RocketMQMetadataProvider.java  |  6 ++-
 .../studio/common/util/PaginationTest.java         | 57 ++++++++++++++++++++++
 .../studio/provider/InstanceProviderTest.java      | 47 ++++++++++++++++++
 .../apache/RocketMQMetadataProviderTest.java       | 35 +++++++++++++
 7 files changed, 193 insertions(+), 6 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/Pagination.java 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/Pagination.java
new file mode 100644
index 000000000..356fcbd64
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/Pagination.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+/** Pagination arithmetic shared by in-memory page slicing across providers. */
+public final class Pagination {
+
+    private Pagination() {
+    }
+
+    /**
+     * Computes the zero-based offset of the requested page with long 
arithmetic, so very
+     * large page numbers never wrap to a negative index. Products that would 
overflow a
+     * {@code long} are capped at {@link Long#MAX_VALUE}; callers treat any 
offset at or
+     * beyond their total element count as an empty page.
+     */
+    public static long pageOffset(long page, int pageSize) {
+        long safePage = Math.max(page, 1L);
+        long size = Math.max(pageSize, 0);
+        if (size == 0) {
+            return 0L;
+        }
+        if (safePage - 1 > Long.MAX_VALUE / size) {
+            return Long.MAX_VALUE;
+        }
+        return (safePage - 1) * size;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
index 9cdecb692..43e64627d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
@@ -17,6 +17,7 @@
 package org.apache.rocketmq.studio.provider;
 
 import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.util.Pagination;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
 import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
 import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
@@ -54,8 +55,9 @@ public interface InstanceProvider {
     default TopicConsumerPageVO getTopicConsumersPage(String instanceId, 
String topicName, int page, int pageSize) {
         List<TopicConsumerVO> consumers = getTopicConsumers(instanceId, 
topicName);
         int total = consumers.size();
-        int from = Math.min((page - 1) * pageSize, total);
-        int to = Math.min(from + pageSize, total);
+        long offset = Pagination.pageOffset(page, pageSize);
+        int from = (int) Math.min(offset, total);
+        int to = from + (int) Math.min(pageSize, total - from);
         return TopicConsumerPageVO.builder()
                 .items(consumers.subList(from, to))
                 .total(total)
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
index f66b7fb17..b565055dd 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
@@ -16,6 +16,7 @@
  */
 package org.apache.rocketmq.studio.provider.apache;
 
+import org.apache.rocketmq.studio.common.util.Pagination;
 import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
 import org.apache.rocketmq.studio.instance.topic.TopicConsumerPageVO;
 import org.apache.rocketmq.studio.instance.topic.BrokerRouteVO;
@@ -40,8 +41,9 @@ public interface MetadataProvider {
     default TopicConsumerPageVO getTopicConsumersPage(String instanceId, 
String name, int page, int pageSize) {
         List<TopicConsumerVO> consumers = getTopicConsumers(instanceId, name);
         int total = consumers.size();
-        int from = Math.min((page - 1) * pageSize, total);
-        int to = Math.min(from + pageSize, total);
+        long offset = Pagination.pageOffset(page, pageSize);
+        int from = (int) Math.min(offset, total);
+        int to = from + (int) Math.min(pageSize, total - from);
         return TopicConsumerPageVO.builder()
                 .items(consumers.subList(from, to))
                 .total(total)
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
index a14711e61..4882beeed 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
@@ -37,6 +37,7 @@ import org.apache.rocketmq.tools.admin.MQAdminExt;
 import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
 import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
 import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.util.Pagination;
 import org.apache.rocketmq.studio.common.util.SystemGroupFilter;
 import org.apache.rocketmq.studio.common.util.SystemTopicFilter;
 import org.apache.rocketmq.studio.common.domain.enums.TopicType;
@@ -312,8 +313,9 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
             List<String> sortedGroups = new ArrayList<>(subscribingGroups);
             sortedGroups.sort(String::compareToIgnoreCase);
             int total = sortedGroups.size();
-            int from = Math.min((page - 1) * pageSize, total);
-            int to = Math.min(from + pageSize, total);
+            long offset = Pagination.pageOffset(page, pageSize);
+            int from = (int) Math.min(offset, total);
+            int to = from + (int) Math.min(pageSize, total - from);
             List<TopicConsumerVO> consumers = new ArrayList<>();
             for (String group : sortedGroups.subList(from, to)) {
                 try {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/PaginationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/PaginationTest.java
new file mode 100644
index 000000000..f7cc5ecba
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/PaginationTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.common.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PaginationTest {
+
+    @Test
+    void pageOffsetShouldComputeZeroBasedOffset() {
+        assertThat(Pagination.pageOffset(1, 20)).isEqualTo(0L);
+        assertThat(Pagination.pageOffset(3, 20)).isEqualTo(40L);
+    }
+
+    @Test
+    void pageOffsetShouldUseLongArithmeticForLargePages() {
+        assertThat(Pagination.pageOffset(Integer.MAX_VALUE, 100))
+                .isEqualTo((Integer.MAX_VALUE - 1L) * 100);
+        assertThat(Pagination.pageOffset(Integer.MAX_VALUE, Integer.MAX_VALUE))
+                .isEqualTo((Integer.MAX_VALUE - 1L) * Integer.MAX_VALUE);
+    }
+
+    @Test
+    void pageOffsetShouldCapWhenLongProductWouldOverflow() {
+        assertThat(Pagination.pageOffset(Long.MAX_VALUE, 
Integer.MAX_VALUE)).isEqualTo(Long.MAX_VALUE);
+        assertThat(Pagination.pageOffset(Long.MAX_VALUE / 2 + 2, 
2)).isEqualTo(Long.MAX_VALUE);
+    }
+
+    @Test
+    void pageOffsetShouldNotCapProductThatExactlyFitsInLong() {
+        assertThat(Pagination.pageOffset(Long.MAX_VALUE, 
1)).isEqualTo(Long.MAX_VALUE - 1);
+    }
+
+    @Test
+    void pageOffsetShouldClampNonPositiveInputsToFirstPage() {
+        assertThat(Pagination.pageOffset(0, 20)).isEqualTo(0L);
+        assertThat(Pagination.pageOffset(-5, 20)).isEqualTo(0L);
+        assertThat(Pagination.pageOffset(5, 0)).isEqualTo(0L);
+        assertThat(Pagination.pageOffset(5, -1)).isEqualTo(0L);
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderTest.java
new file mode 100644
index 000000000..2c0776eaa
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.provider;
+
+import java.util.Arrays;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerPageVO;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class InstanceProviderTest {
+
+    @Test
+    public void getTopicConsumersPageShouldHandleLargePageNumberTest() {
+        InstanceProvider provider = mock(InstanceProvider.class);
+        when(provider.getTopicConsumers("instance-a", 
"orders")).thenReturn(Arrays.asList(
+                TopicConsumerVO.builder().group("group-a").build(),
+                TopicConsumerVO.builder().group("group-b").build()));
+        when(provider.getTopicConsumersPage("instance-a", "orders", 
Integer.MAX_VALUE, 100))
+                .thenCallRealMethod();
+
+        TopicConsumerPageVO result = provider.getTopicConsumersPage(
+                "instance-a", "orders", Integer.MAX_VALUE, 100);
+
+        assertThat(result.getItems()).isEmpty();
+        assertThat(result.getTotal()).isEqualTo(2);
+        assertThat(result.getPage()).isEqualTo(Integer.MAX_VALUE);
+        assertThat(result.getPageSize()).isEqualTo(100);
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
index f8459f52d..9baeeba4e 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
@@ -196,6 +196,41 @@ class RocketMQMetadataProviderTest {
         verify(admin, never()).examineConsumeStats("group-b", "TopicA");
     }
 
+    @Test
+    void getTopicConsumersPageShouldReturnEmptyPageForLargePageNumberTest() 
throws Exception {
+        DefaultMQAdminExt admin = mock(DefaultMQAdminExt.class);
+        GroupList groups = new GroupList();
+        groups.setGroupList(new HashSet<>(List.of("group-a", "group-b")));
+        when(admin.queryTopicConsumeByWho("TopicA")).thenReturn(groups);
+
+        TopicConsumerPageVO result = newLiveProvider(admin)
+                .getTopicConsumersPage(null, "TopicA", Integer.MAX_VALUE, 100);
+
+        assertThat(result.getItems()).isEmpty();
+        assertThat(result.getTotal()).isEqualTo(2);
+        assertThat(result.getPage()).isEqualTo(Integer.MAX_VALUE);
+        assertThat(result.getPageSize()).isEqualTo(100);
+        verify(admin, never()).examineConsumeStats(anyString(), eq("TopicA"));
+    }
+
+    @Test
+    void 
metadataProviderDefaultPageShouldReturnEmptyPageForLargePageNumberTest() {
+        MetadataProvider provider = mock(MetadataProvider.class);
+        when(provider.getTopicConsumers("instance-a", 
"orders")).thenReturn(List.of(
+                TopicConsumerVO.builder().group("group-a").build(),
+                TopicConsumerVO.builder().group("group-b").build()));
+        when(provider.getTopicConsumersPage("instance-a", "orders", 
Integer.MAX_VALUE, 100))
+                .thenCallRealMethod();
+
+        TopicConsumerPageVO result = provider.getTopicConsumersPage(
+                "instance-a", "orders", Integer.MAX_VALUE, 100);
+
+        assertThat(result.getItems()).isEmpty();
+        assertThat(result.getTotal()).isEqualTo(2);
+        assertThat(result.getPage()).isEqualTo(Integer.MAX_VALUE);
+        assertThat(result.getPageSize()).isEqualTo(100);
+    }
+
     @Test
     void groupRuntimeDiagnosticsShouldUseSelectedInstanceRuntimeClient() {
         List<QueueProgressVO> progress = 
List.of(QueueProgressVO.builder().broker("broker-a").build());

Reply via email to