mirror of
https://github.com/ProjectSWGCore/pswgcommon.git
synced 2026-01-17 00:04:25 -05:00
Fixed a bug in CustomizationString#getLength calculation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/***********************************************************************************
|
||||
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
|
||||
* Copyright (c) 2023 /// Project SWG /// www.projectswg.com *
|
||||
* *
|
||||
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
|
||||
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
|
||||
@@ -30,8 +30,8 @@ import com.projectswg.common.data.encodables.mongo.MongoData;
|
||||
import com.projectswg.common.data.encodables.mongo.MongoPersistable;
|
||||
import com.projectswg.common.encoding.Encodable;
|
||||
import com.projectswg.common.network.NetBuffer;
|
||||
import com.projectswg.common.network.NetBufferStream;
|
||||
import me.joshlarson.jlcommon.log.Log;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -49,10 +49,10 @@ import java.util.function.BiConsumer;
|
||||
* facial hair style...
|
||||
*/
|
||||
public class CustomizationString implements Encodable, MongoPersistable {
|
||||
|
||||
|
||||
private static final Map<String, Short> VAR_NAME_TO_ID = new HashMap<>();
|
||||
private static final Map<Short, String> VAR_ID_TO_NAME = new HashMap<>();
|
||||
|
||||
|
||||
static {
|
||||
try (BufferedInputStream bis = new BufferedInputStream(CustomizationString.class.getResourceAsStream("customization_variables.sdb"))) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
@@ -66,7 +66,7 @@ public class CustomizationString implements Encodable, MongoPersistable {
|
||||
break;
|
||||
//noinspection HardcodedLineSeparator
|
||||
case '\r':
|
||||
//noinspection HardcodedLineSeparator
|
||||
//noinspection HardcodedLineSeparator
|
||||
case '\n':
|
||||
if (buffer.length() <= 0)
|
||||
continue;
|
||||
@@ -89,60 +89,71 @@ public class CustomizationString implements Encodable, MongoPersistable {
|
||||
throw new RuntimeException("could not load customization variables from resources", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final Map<String, Integer> variables;
|
||||
|
||||
|
||||
public CustomizationString() {
|
||||
this.variables = Collections.synchronizedMap(new LinkedHashMap<>()); // Ordered and synchronized
|
||||
this.variables = Collections.synchronizedMap(new LinkedHashMap<>()); // Ordered and synchronized
|
||||
}
|
||||
|
||||
|
||||
boolean isEmpty() {
|
||||
return variables.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the amount of characters that would require escaping to be valid UTF-8
|
||||
*/
|
||||
int valuesToEscape() {
|
||||
return (int) variables.values().stream().filter(CustomizationString::isReserved).count();
|
||||
}
|
||||
|
||||
|
||||
int valueLengths() {
|
||||
return variables.values().stream().mapToInt(value -> {
|
||||
boolean singleByte = value <= Byte.MAX_VALUE && value >= Byte.MIN_VALUE;
|
||||
|
||||
if (singleByte) {
|
||||
return 1;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
}).sum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMongo(MongoData data) {
|
||||
data.putMap("variables", variables);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void readMongo(MongoData data) {
|
||||
variables.clear();
|
||||
variables.putAll(data.getMap("variables", String.class, Integer.class));
|
||||
}
|
||||
|
||||
|
||||
public Integer put(String name, int value) {
|
||||
return variables.put(name, value);
|
||||
}
|
||||
|
||||
|
||||
public Integer remove(String name) {
|
||||
return variables.remove(name);
|
||||
}
|
||||
|
||||
|
||||
public Integer get(String name) {
|
||||
return variables.get(name);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Integer> getVariables() {
|
||||
return Collections.unmodifiableMap(variables);
|
||||
}
|
||||
|
||||
|
||||
public void forEach(BiConsumer<? super String, ? super Integer> consumer) {
|
||||
variables.forEach(consumer);
|
||||
}
|
||||
|
||||
|
||||
public void clear() {
|
||||
variables.clear();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
@@ -155,170 +166,169 @@ public class CustomizationString implements Encodable, MongoPersistable {
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void decode(NetBuffer data) {
|
||||
byte[] stringData = data.getArray();
|
||||
|
||||
|
||||
if (stringData.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String string = new String(stringData, StandardCharsets.UTF_8);
|
||||
int[] codePoints = string.codePoints().toArray(); // Replaces 0xC3BF with 0xFF, because 0xFF is reserved as an escape flag
|
||||
int[] codePoints = string.codePoints().toArray(); // Replaces 0xC3BF with 0xFF, because 0xFF is reserved as an escape flag
|
||||
int position = 0;
|
||||
byte startOfText = (byte) codePoints[position++];
|
||||
|
||||
|
||||
if (startOfText != 0x02) {
|
||||
Log.w("Expected UTF8 start-of-text in CustomizationString, assuming corruption!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
short variableCount = (short) codePoints[position++];
|
||||
|
||||
|
||||
if (variableCount == 0xFF) {
|
||||
// SOE decided not to include a variable count for the Twi'lek lekku. No object has anywhere near 255 variables, so we'll be alright.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (short i = 0; i < variableCount; i++) {
|
||||
short combinedVariableInfo = (short) codePoints[position++];
|
||||
int variableSize = ((combinedVariableInfo & 0x80) != 0) ? 2 : 1;
|
||||
short variableId = (short) (combinedVariableInfo & 0x7F);
|
||||
int current = codePoints[position++]; // Read 8-bit value
|
||||
int current = codePoints[position++]; // Read 8-bit value
|
||||
int variable;
|
||||
|
||||
|
||||
switch (variableSize) {
|
||||
case 2:
|
||||
// Read 16-bit value
|
||||
byte lowByte = (byte) current;
|
||||
byte hiByte = (byte) codePoints[position++];
|
||||
position++; // Skip escape
|
||||
|
||||
position++; // Skip escape
|
||||
|
||||
current = ((hiByte << 8) | lowByte) & 0xFF;
|
||||
break;
|
||||
}
|
||||
|
||||
if (current == 0xFF) { // This marks an escaped character to follow
|
||||
|
||||
if (current == 0xFF) { // This marks an escaped character to follow
|
||||
int next = codePoints[position++];
|
||||
|
||||
|
||||
switch (next) {
|
||||
case 0x01: // Value is 0
|
||||
case 0x01: // Value is 0
|
||||
default:
|
||||
variable = 0;
|
||||
break;
|
||||
case 0x02: // Value is 255
|
||||
case 0x02: // Value is 255
|
||||
variable = 0xFF;
|
||||
break;
|
||||
case 0x03: // We shouldn't be meeting an end here. Malformed input.
|
||||
case 0x03: // We shouldn't be meeting an end here. Malformed input.
|
||||
Log.w("Unexpected end of text in CustomizationString, assuming corruption!");
|
||||
clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted
|
||||
clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
variable = current;
|
||||
}
|
||||
|
||||
|
||||
String variableName = VAR_ID_TO_NAME.get(variableId);
|
||||
|
||||
if (variableName == null) { // Variable ID matched no variable name.
|
||||
|
||||
if (variableName == null) { // Variable ID matched no variable name.
|
||||
Log.w("Variable ID %d had no name associated", variableId);
|
||||
position++; // Skip the associated value
|
||||
position++; // Skip the associated value
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
variables.put(variableName, variable);
|
||||
}
|
||||
|
||||
|
||||
if ((position + 2) < codePoints.length) {
|
||||
Log.w("Too much data remaining in CustomizationString, assuming corruption!");
|
||||
clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted
|
||||
clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int escapeFlag = codePoints[position++];
|
||||
int endOfText = codePoints[position];
|
||||
|
||||
|
||||
if (escapeFlag == 0xFF && endOfText != 0x03) {
|
||||
Log.w("Invalid UTF-8 ending for CustomizationString, assuming corruption!");
|
||||
clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted
|
||||
clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encode() {
|
||||
public byte @NotNull [] encode() {
|
||||
if (isEmpty()) {
|
||||
// No need to send more than an empty array in this case
|
||||
return ByteBuffer.allocate(Short.BYTES).array();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(getLength());
|
||||
|
||||
int encodableLength = getLength();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(encodableLength - Short.BYTES);
|
||||
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
|
||||
|
||||
try {
|
||||
out.write(2); // Marks start of text
|
||||
out.write(variables.size());
|
||||
|
||||
variables.forEach((variableName, variable) -> {
|
||||
short combinedVariable = VAR_NAME_TO_ID.get(variableName);
|
||||
|
||||
writer.write(2); // Marks start of text
|
||||
writer.write(variables.size());
|
||||
|
||||
variables.forEach((variableName, variableValue) -> {
|
||||
short variableId = VAR_NAME_TO_ID.get(variableName);
|
||||
|
||||
try {
|
||||
writer.write(combinedVariable); // Put variable
|
||||
|
||||
switch (variable) {
|
||||
case 0x00:
|
||||
writer.write(variableId);
|
||||
|
||||
switch (variableValue) {
|
||||
case 0x00 -> {
|
||||
writer.write(0xFF); // Escape
|
||||
writer.write(0x01); // Put variable value
|
||||
break;
|
||||
case 0xFF:
|
||||
}
|
||||
case 0xFF -> {
|
||||
writer.write(0xFF); // Escape
|
||||
writer.write(0x02); // Put variable value
|
||||
break;
|
||||
default:
|
||||
writer.write(variable);
|
||||
break;
|
||||
}
|
||||
default -> writer.write(variableValue);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(e);
|
||||
}
|
||||
});
|
||||
|
||||
writer.write(0x0FF); // Escape
|
||||
writer.write(3); // Marks end of text
|
||||
|
||||
writer.write(0x0FF); // Escape
|
||||
writer.write(3); // Marks end of text
|
||||
writer.flush();
|
||||
|
||||
|
||||
byte[] result = out.toByteArray();
|
||||
NetBuffer data = NetBuffer.allocate(Short.BYTES + result.length);
|
||||
|
||||
data.addArray(result); // This will add the array length in little endian order
|
||||
|
||||
NetBuffer data = NetBuffer.allocate(encodableLength);
|
||||
|
||||
data.addArray(result); // This will add the array length in little endian order
|
||||
|
||||
return data.array();
|
||||
} catch (IOException e) {
|
||||
Log.e(e);
|
||||
return NetBuffer.allocate(Short.SIZE).array(); // Returns an array of 0x00, 0x00 indicating that it's empty
|
||||
return NetBuffer.allocate(Short.SIZE).array(); // Returns an array of 0x00, 0x00 indicating that it's empty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getLength() {
|
||||
int length = Short.BYTES; // Array size declaration field
|
||||
|
||||
if (isEmpty()) { // No need to send more than an empty array in this case
|
||||
int length = Short.BYTES; // Array size declaration field
|
||||
|
||||
if (isEmpty()) { // No need to send more than an empty array in this case
|
||||
return length;
|
||||
}
|
||||
|
||||
length += 1; // UTF-8 start of text
|
||||
length += 1; // Variable count
|
||||
length += variables.size() * 2; // Amount of variable IDs and their value
|
||||
length += valuesToEscape() * 2; // If there are escaped values in there, there will be 0xC3 0xBF to indicate escape
|
||||
length += 2; // Escape flag
|
||||
length += 1; // UTF-8 end of text
|
||||
|
||||
|
||||
length += 1; // UTF-8 start of text
|
||||
length += 1; // Variable count
|
||||
length += variables.size(); // Variable IDs
|
||||
length += valuesToEscape() * Short.BYTES; // If there are escaped values in there, there will be 0xC3 0xBF to indicate escape
|
||||
length += valueLengths(); // Variable value
|
||||
length += Short.BYTES; // Escape flag
|
||||
length += 1; // UTF-8 end of text
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value the value to check
|
||||
* @return {@code true} if the value is reserved by UTF-8 and escaping it
|
||||
* would be necessary for proper compatibility.
|
||||
@@ -326,5 +336,5 @@ public class CustomizationString implements Encodable, MongoPersistable {
|
||||
private static boolean isReserved(int value) {
|
||||
return value == 0x00 || value == 0xFF;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
***********************************************************************************/
|
||||
package com.projectswg.common.data.customization
|
||||
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import com.projectswg.common.network.NetBuffer
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class TestCustomizationString {
|
||||
@@ -34,8 +35,8 @@ class TestCustomizationString {
|
||||
fun testPut() {
|
||||
val string = CustomizationString()
|
||||
val key = "test"
|
||||
Assertions.assertNull(string.put(key, 0)) // Nothing should be replaced because string's empty
|
||||
Assertions.assertEquals(0, string.put(key, 1)) // Same key, so the variable we put earlier should be replaced
|
||||
assertNull(string.put(key, 0)) // Nothing should be replaced because string's empty
|
||||
assertEquals(0, string.put(key, 1)) // Same key, so the variable we put earlier should be replaced
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -43,29 +44,43 @@ class TestCustomizationString {
|
||||
val string = CustomizationString()
|
||||
val key = "test"
|
||||
string.put(key, 0)
|
||||
Assertions.assertEquals(0, string.remove(key)) // Same key, so the variable we put earlier should be returned
|
||||
assertEquals(0, string.remove(key)) // Same key, so the variable we put earlier should be returned
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsEmpty() {
|
||||
val string = CustomizationString()
|
||||
val key = "test"
|
||||
Assertions.assertTrue(string.isEmpty)
|
||||
assertTrue(string.isEmpty)
|
||||
string.put(key, 0)
|
||||
Assertions.assertFalse(string.isEmpty)
|
||||
assertFalse(string.isEmpty)
|
||||
string.remove(key)
|
||||
Assertions.assertTrue(string.isEmpty)
|
||||
assertTrue(string.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetLength() {
|
||||
fun encoding() {
|
||||
val string = CustomizationString()
|
||||
Assertions.assertEquals(Short.SIZE_BYTES, string.length) // Should be an empty array at this point
|
||||
string.put("first", 7)
|
||||
var expected = Short.SIZE_BYTES + 7
|
||||
Assertions.assertEquals(expected, string.length)
|
||||
string.put("second", 0xFF)
|
||||
expected += 4 // Two escape characters, an ID and a value
|
||||
Assertions.assertEquals(expected, string.length)
|
||||
string.put("/private/index_color_1", 237)
|
||||
string.put("/private/index_color_2", 4)
|
||||
val expected = byteArrayOf(10, 0, 2, 2, 2, -61, -83, 1, 4, -61, -65, 3)
|
||||
|
||||
val actual = string.encode()
|
||||
|
||||
assertArrayEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decoding() {
|
||||
val string = CustomizationString()
|
||||
string.put("/private/index_color_1", 237)
|
||||
string.put("/private/index_color_2", 4)
|
||||
val input = byteArrayOf(10, 0, 2, 2, 2, -61, -83, 1, 4, -61, -65, 3)
|
||||
|
||||
string.decode(NetBuffer.wrap(input))
|
||||
|
||||
assertEquals(2, string.variables.size)
|
||||
assertEquals(string.get("/private/index_color_1"), 237)
|
||||
assertEquals(string.get("/private/index_color_2"), 4)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user