Fix CatalogPagesListComposer parsing error in Nitro client

The Nitro client's NodeData parser enforces safety limits (max 1000 offers,
500 children, 20 depth) using Math.min() on the count but the server was
sending all data beyond those limits. The unread bytes left in the buffer
corrupted parsing of subsequent nodes, causing RangeError.

Cap server-side output to match client limits and snapshot offerIds array
to prevent potential race conditions between size() and iteration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Life
2026-03-24 21:47:54 +01:00
parent 195525a997
commit 0a56f46d28
@@ -15,6 +15,10 @@ import java.util.List;
public class CatalogPagesListComposer extends MessageComposer {
private static final Logger LOGGER = LoggerFactory.getLogger(CatalogPagesListComposer.class);
private static final int MAX_OFFERS = 1000;
private static final int MAX_CHILDREN = 500;
private static final int MAX_DEPTH = 20;
private final Habbo habbo;
private final String mode;
private final boolean hasPermission;
@@ -38,10 +42,12 @@ public class CatalogPagesListComposer extends MessageComposer {
this.response.appendString("root");
this.response.appendString("");
this.response.appendInt(0);
this.response.appendInt(pages.size());
for (CatalogPage category : pages) {
this.append(category);
int childCount = Math.min(pages.size(), MAX_CHILDREN);
this.response.appendInt(childCount);
for (int idx = 0; idx < childCount; idx++) {
this.append(pages.get(idx), 1);
}
this.response.appendBoolean(false);
@@ -55,7 +61,7 @@ public class CatalogPagesListComposer extends MessageComposer {
return null;
}
private void append(CatalogPage category) {
private void append(CatalogPage category, int depth) {
List<CatalogPage> pagesList = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(category.getId(), this.habbo);
this.response.appendBoolean(category.isVisible());
@@ -63,16 +69,25 @@ public class CatalogPagesListComposer extends MessageComposer {
this.response.appendInt(category.isEnabled() ? category.getId() : -1);
this.response.appendString(category.getPageName());
this.response.appendString(category.getCaption() + (this.hasPermission ? " (" + category.getId() + ")" : ""));
this.response.appendInt(category.getOfferIds().size());
for (int i : category.getOfferIds().toArray()) {
this.response.appendInt(i);
int[] offers = category.getOfferIds().toArray();
int offerCount = Math.min(offers.length, MAX_OFFERS);
this.response.appendInt(offerCount);
for (int idx = 0; idx < offerCount; idx++) {
this.response.appendInt(offers[idx]);
}
this.response.appendInt(pagesList.size());
if (depth >= MAX_DEPTH) {
this.response.appendInt(0);
return;
}
for (CatalogPage page : pagesList) {
this.append(page);
int childCount = Math.min(pagesList.size(), MAX_CHILDREN);
this.response.appendInt(childCount);
for (int idx = 0; idx < childCount; idx++) {
this.append(pagesList.get(idx), depth + 1);
}
}