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