Skip to content

Commit 2ca6a83

Browse files
committed
bugfix(pagination): prevent integer overflow and clamp page/size safely
1 parent 1d42b8d commit 2ca6a83

File tree

3 files changed

+58
-27
lines changed

3 files changed

+58
-27
lines changed

customer-service/src/main/java/io/github/bsayli/customerservice/common/api/response/Page.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,17 @@ public record Page<T>(
1818
boolean hasPrev) {
1919

2020
public static <T> Page<T> of(List<T> content, int page, int size, long totalElements) {
21-
List<T> safeContent = content == null ? List.of() : List.copyOf(content);
22-
int totalPages = (int) Math.ceil((double) totalElements / size);
23-
boolean hasNext = page + 1 < totalPages;
24-
boolean hasPrev = page > 0;
25-
return new Page<>(safeContent, page, size, totalElements, totalPages, hasNext, hasPrev);
21+
List<T> safeContent = (content == null) ? List.of() : List.copyOf(content);
22+
23+
int p = Math.clamp(page, 0, Integer.MAX_VALUE);
24+
int s = Math.clamp(size, 1, Integer.MAX_VALUE);
25+
26+
long totalPagesL = (totalElements <= 0L) ? 0L : ((totalElements + s - 1L) / s);
27+
int totalPages = (totalPagesL > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) totalPagesL;
28+
29+
boolean hasNext = p < totalPages - 1;
30+
boolean hasPrev = p > 0;
31+
32+
return new Page<>(safeContent, p, s, totalElements, totalPages, hasNext, hasPrev);
2633
}
2734
}

customer-service/src/main/java/io/github/bsayli/customerservice/service/impl/CustomerServiceImpl.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
@Service
2121
public class CustomerServiceImpl implements CustomerService {
2222

23+
private static final int MAX_PAGE_SIZE = 10;
2324
private final AtomicInteger idSeq = new AtomicInteger(0);
2425
private final NavigableMap<Integer, CustomerDto> store = new ConcurrentSkipListMap<>();
2526

@@ -115,10 +116,17 @@ private Comparator<CustomerDto> buildComparator(SortField sortBy, SortDirection
115116
}
116117

117118
private Page<CustomerDto> paginate(List<CustomerDto> items, int page, int size) {
119+
int p = Math.clamp(page, 0, Integer.MAX_VALUE);
120+
int s = Math.clamp(size, 1, MAX_PAGE_SIZE);
121+
118122
long total = items.size();
119-
int from = Math.min(page * size, items.size());
120-
int to = Math.min(from + size, items.size());
123+
long fromL = Math.min((long) p * s, total);
124+
long toL = Math.min(fromL + s, total);
125+
126+
int from = (int) fromL;
127+
int to = (int) toL;
128+
121129
var slice = items.subList(from, to);
122-
return Page.of(slice, page, size, total);
130+
return Page.of(slice, p, s, total);
123131
}
124132
}

customer-service/src/test/java/io/github/bsayli/customerservice/service/impl/CustomerServiceImplTest.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,35 @@ void setUp() {
2828
}
2929

3030
@Test
31-
@DisplayName("Initial seed should contain 17 customers")
32-
void initialSeed_shouldContainSeventeen() {
33-
Page<CustomerDto> page =
31+
@DisplayName("Initial seed pagination: total=17, page0 size=10, page1 size=7")
32+
void initialSeed_paginationShouldBeCorrect() {
33+
Page<CustomerDto> p0 =
3434
service.getCustomers(
3535
new CustomerSearchCriteria(null, null),
3636
0,
3737
100,
3838
SortField.CUSTOMER_ID,
3939
SortDirection.ASC);
4040

41-
assertEquals(17, page.totalElements());
42-
assertEquals(17, page.content().size());
43-
assertTrue(page.content().stream().allMatch(c -> c.customerId() != null && c.customerId() > 0));
41+
assertEquals(17, p0.totalElements());
42+
assertEquals(2, p0.totalPages());
43+
assertEquals(10, p0.content().size());
44+
assertTrue(p0.hasNext());
45+
assertFalse(p0.hasPrev());
46+
47+
Page<CustomerDto> p1 =
48+
service.getCustomers(
49+
new CustomerSearchCriteria(null, null),
50+
1,
51+
100,
52+
SortField.CUSTOMER_ID,
53+
SortDirection.ASC);
54+
55+
assertEquals(17, p1.totalElements());
56+
assertEquals(2, p1.totalPages());
57+
assertEquals(7, p1.content().size());
58+
assertFalse(p1.hasNext());
59+
assertTrue(p1.hasPrev());
4460
}
4561

4662
@Test
@@ -54,26 +70,27 @@ void createCustomer_shouldAssignIdAndStore() {
5470
assertEquals("Jane Doe", created.name());
5571
assertEquals("jane.doe@example.com", created.email());
5672

57-
Page<CustomerDto> page =
73+
Page<CustomerDto> anyPage =
5874
service.getCustomers(
5975
new CustomerSearchCriteria(null, null),
6076
0,
61-
200,
77+
10,
6278
SortField.CUSTOMER_ID,
6379
SortDirection.ASC);
6480

65-
assertEquals(18, page.totalElements());
66-
assertTrue(page.content().stream().anyMatch(c -> c.customerId().equals(created.customerId())));
81+
assertEquals(18, anyPage.totalElements());
82+
CustomerDto fetched = service.getCustomer(created.customerId());
83+
assertEquals(created, fetched);
6784
}
6885

6986
@Test
7087
@DisplayName("getCustomer should return existing customer")
7188
void getCustomer_shouldReturn() {
72-
Page<CustomerDto> page =
89+
Page<CustomerDto> p0 =
7390
service.getCustomers(
7491
new CustomerSearchCriteria(null, null), 0, 1, SortField.CUSTOMER_ID, SortDirection.ASC);
7592

76-
CustomerDto any = page.content().getFirst();
93+
CustomerDto any = p0.content().getFirst();
7794
CustomerDto found = service.getCustomer(any.customerId());
7895
assertEquals(any, found);
7996
}
@@ -106,7 +123,7 @@ void updateCustomer_shouldThrowWhenMissing() {
106123
}
107124

108125
@Test
109-
@DisplayName("deleteCustomer should remove the record")
126+
@DisplayName("deleteCustomer should remove the record and decrease total")
110127
void deleteCustomer_shouldRemove() {
111128
CustomerDto base =
112129
service.createCustomer(new CustomerCreateRequest("Mark Lee", "mark.lee@example.com"));
@@ -115,22 +132,21 @@ void deleteCustomer_shouldRemove() {
115132
service.getCustomers(
116133
new CustomerSearchCriteria(null, null),
117134
0,
118-
200,
135+
10,
119136
SortField.CUSTOMER_ID,
120137
SortDirection.ASC);
121-
long sizeBefore = before.totalElements();
138+
long totalBefore = before.totalElements();
122139

123140
service.deleteCustomer(base.customerId());
124141

125142
Page<CustomerDto> after =
126143
service.getCustomers(
127144
new CustomerSearchCriteria(null, null),
128145
0,
129-
200,
146+
10,
130147
SortField.CUSTOMER_ID,
131148
SortDirection.ASC);
132-
133-
assertEquals(sizeBefore - 1, after.totalElements());
134-
assertTrue(after.content().stream().noneMatch(c -> c.customerId().equals(base.customerId())));
149+
assertEquals(totalBefore - 1, after.totalElements());
150+
assertThrows(NoSuchElementException.class, () -> service.getCustomer(base.customerId()));
135151
}
136152
}

0 commit comments

Comments
 (0)