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) { + } + +}