diff --git a/pom.xml b/pom.xml
index daa52ee..4a0854d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,6 +11,7 @@
25
25
4.0.1
+ 1.5.7-6
@@ -30,13 +31,25 @@
org.springframework.boot
spring-boot-starter
+
+ com.github.luben
+ zstd-jni
+ ${zstd-jni.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-jackson
+
+
+ tools.jackson.dataformat
+ jackson-dataformat-yaml
+
org.springframework.boot
spring-boot-starter-test
test
-
org.awaitility
awaitility
diff --git a/src/main/java/de/extio/game_engine/util/ObjectSerialization.java b/src/main/java/de/extio/game_engine/util/ObjectSerialization.java
new file mode 100644
index 0000000..3a883dd
--- /dev/null
+++ b/src/main/java/de/extio/game_engine/util/ObjectSerialization.java
@@ -0,0 +1,135 @@
+package de.extio.game_engine.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.DigestInputStream;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.function.Consumer;
+
+import com.github.luben.zstd.ZstdInputStream;
+import com.github.luben.zstd.ZstdOutputStream;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.dataformat.yaml.YAMLMapper;
+
+public final class ObjectSerialization {
+
+ private static final ObjectMapper OBJECT_MAPPER;
+
+ static {
+ OBJECT_MAPPER = YAMLMapper.builder()
+ .findAndAddModules()
+ .build();
+ }
+
+ public byte[] serialize(final Object o, final boolean compress, final boolean base64, final boolean digest, final byte[] digestSalt, final Consumer digestConsumer) {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1 << 16);
+ OutputStream base64OutputStream = null;
+ ZstdOutputStream zstdOutputStream = null;
+ DigestOutputStream digestOutputStream = null;
+ try {
+ OutputStream stream = byteArrayOutputStream;
+ if (base64) {
+ stream = base64OutputStream = Base64.getEncoder().wrap(stream);
+ }
+ if (compress) {
+ stream = zstdOutputStream = new ZstdOutputStream(stream);
+ }
+ if (digest) {
+ stream = digestOutputStream = new DigestOutputStream(stream, MessageDigest.getInstance("SHA-256"));
+ }
+ OBJECT_MAPPER.writeValue(stream, o);
+ }
+ catch (IOException | NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ finally {
+ if (digestOutputStream != null) {
+ try {
+ digestOutputStream.close();
+ }
+ catch (final IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ digestConsumer.accept(digestOutputStream.getMessageDigest().digest(digestSalt));
+ }
+ if (zstdOutputStream != null) {
+ try {
+ zstdOutputStream.close();
+ }
+ catch (final IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ if (base64OutputStream != null) {
+ try {
+ base64OutputStream.close();
+ }
+ catch (final IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ }
+
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ public T deserialize(final Class clazz, final byte[] data, final boolean deserializeObjRefs, final boolean decompress, final boolean base64, final byte[] checkDigest, final byte[] digestSalt, final Consumer digestCheckConsumer) {
+ final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
+ InputStream base64InputStream = null;
+ ZstdInputStream zstdInputStream = null;
+ DigestInputStream digestInputStream = null;
+ try {
+ InputStream stream = byteArrayInputStream;
+ if (base64) {
+ stream = base64InputStream = Base64.getDecoder().wrap(stream);
+ }
+ if (decompress) {
+ stream = zstdInputStream = new ZstdInputStream(stream);
+ }
+ if (checkDigest != null && checkDigest.length > 0) {
+ stream = digestInputStream = new DigestInputStream(stream, MessageDigest.getInstance("SHA-256"));
+ }
+
+ return OBJECT_MAPPER.readValue(stream, clazz);
+ }
+ catch (NoSuchAlgorithmException | IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ finally {
+ if (base64InputStream != null) {
+ try {
+ base64InputStream.close();
+ }
+ catch (final IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ if (zstdInputStream != null) {
+ try {
+ zstdInputStream.close();
+ }
+ catch (final IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ if (digestInputStream != null) {
+ try {
+ digestInputStream.close();
+ digestCheckConsumer.accept(Arrays.equals(checkDigest, digestInputStream.getMessageDigest().digest(digestSalt)));
+ }
+ catch (final IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/test/java/de/extio/game_engine/util/ObjectSerializationTest.java b/src/test/java/de/extio/game_engine/util/ObjectSerializationTest.java
new file mode 100644
index 0000000..fa822a5
--- /dev/null
+++ b/src/test/java/de/extio/game_engine/util/ObjectSerializationTest.java
@@ -0,0 +1,148 @@
+package de.extio.game_engine.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.Map;
+
+public class ObjectSerializationTest {
+
+ @Test
+ public void testSerializeDeserializeBasic() {
+ var serialization = new ObjectSerialization();
+ var original = new TestObject("Hello", 42, 3.14, List.of("tag1", "tag2"), List.of(new TestObject2("key", true), new TestObject2("key2", false)), Map.of("attr", 1, "attr2", 2));
+
+ var serialized = serialization.serialize(original, false, false, false, null, digest -> {
+ });
+
+ System.out.println("Serialized length: " + serialized.length);
+ System.out.println("" + new String(serialized));
+ assertNotNull(serialized);
+ assertTrue(serialized.length > 0);
+
+ var deserialized = serialization.deserialize(TestObject.class, serialized, false, false, false, null, null, result -> {
+ });
+
+ assertNotNull(deserialized);
+ assertEquals(original.name, deserialized.name);
+ assertEquals(original.id, deserialized.id);
+ assertEquals(original.value, deserialized.value);
+ }
+
+ @Test
+ public void testSerializeDeserializeWithCompression() {
+ var serialization = new ObjectSerialization();
+ var original = new TestObject("Compressed", 100, 2.71, List.of("a", "b", "c"), List.of(), Map.of());
+
+ var serialized = serialization.serialize(original, true, false, false, null, digest -> {
+ });
+
+ assertNotNull(serialized);
+ assertTrue(serialized.length > 0);
+
+ var deserialized = serialization.deserialize(TestObject.class, serialized, false, true, false, null, null, result -> {
+ });
+
+ assertNotNull(deserialized);
+ assertEquals(original.name, deserialized.name);
+ assertEquals(original.id, deserialized.id);
+ assertEquals(original.value, deserialized.value);
+ }
+
+ @Test
+ public void testSerializeDeserializeWithBase64() {
+ var serialization = new ObjectSerialization();
+ var original = new TestObject("Base64", 200, 1.41, List.of(), List.of(), Map.of());
+
+ var serialized = serialization.serialize(original, false, true, false, null, digest -> {
+ });
+
+ assertNotNull(serialized);
+ assertTrue(serialized.length > 0);
+ assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
+
+ var deserialized = serialization.deserialize(TestObject.class, serialized, false, false, true, null, null, result -> {
+ });
+
+ assertNotNull(deserialized);
+ assertEquals(original.name, deserialized.name);
+ assertEquals(original.id, deserialized.id);
+ assertEquals(original.value, deserialized.value);
+ }
+
+ @Test
+ public void testSerializeDeserializeWithDigest() {
+ var serialization = new ObjectSerialization();
+ var original = new TestObject("Digested", 300, 0.5, List.of("x"), List.of(), Map.of());
+ var digestSalt = "salt".getBytes();
+ var capturedDigest = new byte[32][];
+
+ var serialized = serialization.serialize(original, false, false, true, digestSalt, digest -> capturedDigest[0] = digest);
+
+ assertNotNull(serialized);
+ assertNotNull(capturedDigest[0]);
+ assertTrue(capturedDigest[0].length == 32);
+
+ var digestCheckPassed = new boolean[1];
+ var deserialized = serialization.deserialize(TestObject.class, serialized, false, false, false, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
+
+ assertNotNull(deserialized);
+ assertTrue(digestCheckPassed[0]);
+ assertEquals(original.name, deserialized.name);
+ assertEquals(original.id, deserialized.id);
+ assertEquals(original.value, deserialized.value);
+ }
+
+ @Test
+ public void testSerializeDeserializeWithCompressionAndBase64() {
+ var serialization = new ObjectSerialization();
+ var original = new TestObject("CompressedBase64", 400, 1.23, List.of("p", "q"), List.of(new TestObject2("nested", true)), Map.of("key", 99));
+
+ var serialized = serialization.serialize(original, true, true, false, null, digest -> {
+ });
+
+ assertNotNull(serialized);
+ assertTrue(serialized.length > 0);
+ assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
+
+ var deserialized = serialization.deserialize(TestObject.class, serialized, false, true, true, null, null, result -> {
+ });
+
+ assertNotNull(deserialized);
+ assertEquals(original.name, deserialized.name);
+ assertEquals(original.id, deserialized.id);
+ assertEquals(original.value, deserialized.value);
+ }
+
+ @Test
+ public void testSerializeDeserializeWithAllOptions() {
+ var serialization = new ObjectSerialization();
+ var original = new TestObject("AllOptions", 500, 9.99, List.of("all"), List.of(new TestObject2("a", false), new TestObject2("b", true)), Map.of("x", 1, "y", 2));
+ var digestSalt = "salt123".getBytes();
+ var capturedDigest = new byte[32][];
+
+ var serialized = serialization.serialize(original, true, true, true, digestSalt, digest -> capturedDigest[0] = digest);
+
+ assertNotNull(serialized);
+ assertNotNull(capturedDigest[0]);
+ assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
+
+ var digestCheckPassed = new boolean[1];
+ var deserialized = serialization.deserialize(TestObject.class, serialized, false, true, true, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
+
+ assertNotNull(deserialized);
+ assertTrue(digestCheckPassed[0]);
+ assertEquals(original.name, deserialized.name);
+ assertEquals(original.id, deserialized.id);
+ assertEquals(original.value, deserialized.value);
+ }
+
+ public record TestObject(String name, int id, double value, List tags, List children, Map attributes) {
+ }
+
+ public record TestObject2(String key, boolean flag) {
+ }
+
+}