You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
feat(items): FurnitureTextProvider — volatile index, sanitize, toggle
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory index of furnidata display names, keyed by the lowercased base
|
||||||
|
* classname (the {@code *N} colour-variant suffix is stripped). Read lazily by
|
||||||
|
* {@link Item#getDisplayName()}. Names are sanitized at index time.
|
||||||
|
*
|
||||||
|
* Thread-safety: the index is held behind a {@code volatile} reference; readers
|
||||||
|
* never block; {@link #reindex(List)} builds a fresh map and swaps it atomically.
|
||||||
|
*/
|
||||||
|
public class FurnitureTextProvider {
|
||||||
|
|
||||||
|
private static final int MAX_LEN = 256;
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
private volatile Map<String, FurniText> index = Map.of();
|
||||||
|
|
||||||
|
public FurnitureTextProvider(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a fresh sanitized index from the given entries and swap it in atomically. */
|
||||||
|
public void reindex(List<FurnidataEntry> entries) {
|
||||||
|
Map<String, FurniText> next = new HashMap<>(Math.max(16, entries.size() * 2));
|
||||||
|
for (FurnidataEntry e : entries) {
|
||||||
|
String key = baseKey(e.classname());
|
||||||
|
if (key == null) continue;
|
||||||
|
next.put(key, new FurniText(e.id(), e.type(), sanitize(e.name()), sanitize(e.description())));
|
||||||
|
}
|
||||||
|
this.index = next; // atomic reference swap
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the sanitized display name for a DB classname, or null if absent/disabled. */
|
||||||
|
public String getName(String classname) {
|
||||||
|
if (!this.enabled) return null;
|
||||||
|
String key = baseKey(classname);
|
||||||
|
if (key == null) return null;
|
||||||
|
FurniText t = this.index.get(key);
|
||||||
|
return (t != null) ? t.name() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String baseKey(String classname) {
|
||||||
|
if (classname == null) return null;
|
||||||
|
int star = classname.indexOf('*');
|
||||||
|
String base = (star >= 0) ? classname.substring(0, star) : classname;
|
||||||
|
base = base.trim().toLowerCase();
|
||||||
|
return base.isEmpty() ? null : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cap length, strip control chars/newlines, neutralize % (placeholder-injection safe). */
|
||||||
|
static String sanitize(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder(Math.min(value.length(), MAX_LEN));
|
||||||
|
for (int i = 0; i < value.length() && sb.length() < MAX_LEN; i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
if (c == '%') { sb.append('%'); continue; } // fullwidth percent — not a placeholder token
|
||||||
|
if (c == '\n' || c == '\r' || Character.isISOControl(c)) continue;
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record FurniText(int id, FurnitureType type, String name, String description) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class FurnitureTextProviderTest {
|
||||||
|
|
||||||
|
private FurnitureTextProvider provider(boolean enabled, FurnidataEntry... entries) {
|
||||||
|
FurnitureTextProvider p = new FurnitureTextProvider(enabled);
|
||||||
|
p.reindex(List.of(entries));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesNameByClassname() {
|
||||||
|
FurnitureTextProvider p = provider(true,
|
||||||
|
new FurnidataEntry(1, "chair_norja", FurnitureType.FLOOR, "Norja Chair", "Sit"));
|
||||||
|
assertEquals("Norja Chair", p.getName("chair_norja"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesBaseClassnameIgnoringColourVariantAndCase() {
|
||||||
|
FurnitureTextProvider p = provider(true,
|
||||||
|
new FurnidataEntry(1, "chair_norja*2", FurnitureType.FLOOR, "Norja Chair", "Sit"));
|
||||||
|
assertEquals("Norja Chair", p.getName("CHAIR_NORJA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsNullWhenClassnameMissing() {
|
||||||
|
FurnitureTextProvider p = provider(true);
|
||||||
|
assertNull(p.getName("unknown_thing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsNullWhenDisabled() {
|
||||||
|
FurnitureTextProvider p = provider(false,
|
||||||
|
new FurnidataEntry(1, "chair_norja", FurnitureType.FLOOR, "Norja Chair", "Sit"));
|
||||||
|
assertNull(p.getName("chair_norja"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sanitizesNameCapStripControlAndNeutralizesPercent() {
|
||||||
|
String evil = "Bad\nName %limit% %user.name%".repeat(20);
|
||||||
|
FurnitureTextProvider p = provider(true,
|
||||||
|
new FurnidataEntry(1, "x", FurnitureType.FLOOR, evil, ""));
|
||||||
|
String name = p.getName("x");
|
||||||
|
assertTrue(name.length() <= 256, "must be capped to 256");
|
||||||
|
assertFalse(name.chars().anyMatch(Character::isISOControl), "no control chars remain after sanitize");
|
||||||
|
assertFalse(name.contains("%"), "ASCII percent neutralized");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullProviderNameNeverThrows() {
|
||||||
|
FurnitureTextProvider p = provider(true);
|
||||||
|
assertDoesNotThrow(() -> p.getName(null));
|
||||||
|
assertNull(p.getName(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user