aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/mulk/jgvariant/core/Decoder.java418
-rw-r--r--src/main/java/eu/mulk/jgvariant/core/Value.java35
-rw-r--r--src/main/java/module-info.java7
-rw-r--r--src/test/java/eu/mulk/jgvariant/core/DecoderTest.java278
4 files changed, 738 insertions, 0 deletions
diff --git a/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/src/main/java/eu/mulk/jgvariant/core/Decoder.java
new file mode 100644
index 0000000..2ebd0af
--- /dev/null
+++ b/src/main/java/eu/mulk/jgvariant/core/Decoder.java
@@ -0,0 +1,418 @@
+package eu.mulk.jgvariant.core;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+import eu.mulk.jgvariant.core.Value.Array;
+import eu.mulk.jgvariant.core.Value.Bool;
+import eu.mulk.jgvariant.core.Value.Float64;
+import eu.mulk.jgvariant.core.Value.Int16;
+import eu.mulk.jgvariant.core.Value.Int32;
+import eu.mulk.jgvariant.core.Value.Int64;
+import eu.mulk.jgvariant.core.Value.Int8;
+import eu.mulk.jgvariant.core.Value.Maybe;
+import eu.mulk.jgvariant.core.Value.Str;
+import eu.mulk.jgvariant.core.Value.Structure;
+import eu.mulk.jgvariant.core.Value.Variant;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.RecordComponent;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Type class for decodable {@link Value} types.
+ *
+ * <p>Use the {@code of*} family of constructor methods to acquire a suitable {@link Decoder} for
+ * the type you wish to decode.
+ *
+ * @param <T> the type that the {@link Decoder} can decode.
+ */
+@SuppressWarnings("java:S1610")
+public abstract class Decoder<T extends Value> {
+
+ private Decoder() {}
+
+ /**
+ * @throws java.nio.BufferUnderflowException if the byte buffer is shorter than the requested
+ * data.
+ */
+ public abstract T decode(ByteBuffer byteSlice);
+
+ abstract byte alignment();
+
+ @Nullable
+ abstract Integer fixedSize();
+
+ final boolean hasFixedSize() {
+ return fixedSize() != null;
+ }
+
+ public Decoder<T> withByteOrder(ByteOrder byteOrder) {
+ var delegate = this;
+
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return delegate.alignment();
+ }
+
+ @Override
+ public @Nullable Integer fixedSize() {
+ return delegate.fixedSize();
+ }
+
+ @Override
+ public T decode(ByteBuffer byteSlice) {
+ byteSlice.order(byteOrder);
+ return delegate.decode(byteSlice);
+ }
+ };
+ }
+
+ public static <U extends Value> Decoder<Array<U>> ofArray(Decoder<U> elementDecoder) {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return elementDecoder.alignment();
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public Array<U> decode(ByteBuffer byteSlice) {
+ List<U> elements;
+
+ var elementSize = elementDecoder.fixedSize();
+ if (elementSize != null) {
+ // A simple C-style array.
+ elements = new ArrayList<>(byteSlice.limit() / elementSize);
+ for (int i = 0; i < byteSlice.limit(); i += elementSize) {
+ var element = elementDecoder.decode(byteSlice.slice(i, elementSize));
+ elements.add(element);
+ }
+ } else {
+ // An array with aligned elements and a vector of framing offsets in the end.
+ int framingOffsetSize = byteCount(byteSlice.limit());
+ int lastFramingOffset =
+ getIntN(byteSlice.slice(byteSlice.limit() - framingOffsetSize, framingOffsetSize));
+ int elementCount = (byteSlice.limit() - lastFramingOffset) / framingOffsetSize;
+
+ elements = new ArrayList<>(elementCount);
+ int position = 0;
+ for (int i = 0; i < elementCount; i++) {
+ int framingOffset =
+ getIntN(
+ byteSlice.slice(lastFramingOffset + i * framingOffsetSize, framingOffsetSize));
+ elements.add(
+ elementDecoder.decode(byteSlice.slice(position, framingOffset - position)));
+ position = align(framingOffset, alignment());
+ }
+ }
+
+ return new Array<>(elements);
+ }
+ };
+ }
+
+ private static int align(int offset, byte alignment) {
+ return offset % alignment == 0 ? offset : offset + alignment - (offset % alignment);
+ }
+
+ private static int getIntN(ByteBuffer byteSlice) {
+ var intBytes = new byte[4];
+ byteSlice.get(intBytes, 0, Math.min(4, byteSlice.limit()));
+ return ByteBuffer.wrap(intBytes).order(LITTLE_ENDIAN).getInt();
+ }
+
+ @SuppressWarnings("java:S3358")
+ private static int byteCount(int n) {
+ return n < (1 << 8) ? 1 : n < (1 << 16) ? 2 : 4;
+ }
+
+ public static <U extends Value> Decoder<Maybe<U>> ofMaybe(Decoder<U> elementDecoder) {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return elementDecoder.alignment();
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public Maybe<U> decode(ByteBuffer byteSlice) {
+ if (!byteSlice.hasRemaining()) {
+ return new Maybe<>(Optional.empty());
+ } else {
+ if (!elementDecoder.hasFixedSize()) {
+ // Remove trailing zero byte.
+ byteSlice.limit(byteSlice.limit() - 1);
+ }
+
+ return new Maybe<>(Optional.of(elementDecoder.decode(byteSlice)));
+ }
+ }
+ };
+ }
+
+ @SafeVarargs
+ public static <U extends Record> Decoder<Structure<U>> ofStructure(
+ Class<U> recordType, Decoder<? extends Value>... componentDecoders) {
+ var recordComponents = recordType.getRecordComponents();
+ if (componentDecoders.length != recordComponents.length) {
+ throw new IllegalArgumentException(
+ "number of decoders (%d) does not match number of structure components (%d)"
+ .formatted(componentDecoders.length, recordComponents.length));
+ }
+
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return (byte) Arrays.stream(componentDecoders).mapToInt(Decoder::alignment).max().orElse(1);
+ }
+
+ @Override
+ public Integer fixedSize() {
+ int position = 0;
+ for (var componentDecoder : componentDecoders) {
+ var fixedComponentSize = componentDecoder.fixedSize();
+ if (fixedComponentSize == null) {
+ return null;
+ }
+
+ position = align(position, componentDecoder.alignment());
+ position += fixedComponentSize;
+ }
+
+ if (position == 0) {
+ return 1;
+ }
+
+ return align(position, alignment());
+ }
+
+ @Override
+ public Structure<U> decode(ByteBuffer byteSlice) {
+ int framingOffsetSize = byteCount(byteSlice.limit());
+
+ var recordConstructorArguments = new Object[recordComponents.length];
+
+ int position = 0;
+ int framingOffsetIndex = 0;
+ int componentIndex = 0;
+ for (var componentDecoder : componentDecoders) {
+ position = align(position, componentDecoder.alignment());
+
+ var fixedComponentSize = componentDecoder.fixedSize();
+ if (fixedComponentSize != null) {
+ recordConstructorArguments[componentIndex] =
+ componentDecoder.decode(byteSlice.slice(position, fixedComponentSize));
+ position += fixedComponentSize;
+ } else {
+ if (componentIndex == recordComponents.length - 1) {
+ // The last component never has a framing offset.
+ int endPosition = byteSlice.limit() - framingOffsetIndex * framingOffsetSize;
+ recordConstructorArguments[componentIndex] =
+ componentDecoder.decode(byteSlice.slice(position, endPosition - position));
+ position = endPosition;
+ } else {
+ int framingOffset =
+ getIntN(
+ byteSlice.slice(
+ byteSlice.limit() - (1 + framingOffsetIndex) * framingOffsetSize,
+ framingOffsetSize));
+ recordConstructorArguments[componentIndex] =
+ componentDecoder.decode(byteSlice.slice(position, framingOffset - position));
+ position = framingOffset;
+ ++framingOffsetIndex;
+ }
+ }
+
+ ++componentIndex;
+ }
+
+ try {
+ var recordComponentTypes =
+ Arrays.stream(recordType.getRecordComponents())
+ .map(RecordComponent::getType)
+ .toArray(Class<?>[]::new);
+ var recordConstructor = recordType.getDeclaredConstructor(recordComponentTypes);
+ return new Structure<>(recordConstructor.newInstance(recordConstructorArguments));
+ } catch (NoSuchMethodException
+ | InstantiationException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ };
+ }
+
+ public static Decoder<Variant> ofVariant() {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 8;
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public Variant decode(ByteBuffer byteSlice) {
+ // TODO
+ throw new UnsupportedOperationException("not implemented");
+ }
+ };
+ }
+
+ public static Decoder<Bool> ofBoolean() {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 1;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 1;
+ }
+
+ @Override
+ public Bool decode(ByteBuffer byteSlice) {
+ return new Bool(byteSlice.get() != 0);
+ }
+ };
+ }
+
+ public static Decoder<Int8> ofInt8() {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 1;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 1;
+ }
+
+ @Override
+ public Int8 decode(ByteBuffer byteSlice) {
+ return new Int8(byteSlice.get());
+ }
+ };
+ }
+
+ public static Decoder<Int16> ofInt16() {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 2;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 2;
+ }
+
+ @Override
+ public Int16 decode(ByteBuffer byteSlice) {
+ return new Int16(byteSlice.getShort());
+ }
+ };
+ }
+
+ public static Decoder<Int32> ofInt32() {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 4;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 4;
+ }
+
+ @Override
+ public Int32 decode(ByteBuffer byteSlice) {
+ return new Int32(byteSlice.getInt());
+ }
+ };
+ }
+
+ public static Decoder<Int64> ofInt64() {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 8;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 8;
+ }
+
+ @Override
+ public Int64 decode(ByteBuffer byteSlice) {
+ return new Int64(byteSlice.getLong());
+ }
+ };
+ }
+
+ public static Decoder<Float64> ofFloat64() {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 8;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 8;
+ }
+
+ @Override
+ public Float64 decode(ByteBuffer byteSlice) {
+ return new Float64(byteSlice.getDouble());
+ }
+ };
+ }
+
+ public static Decoder<Str> ofStr(Charset charset) {
+ return new Decoder<>() {
+ @Override
+ public byte alignment() {
+ return 1;
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public Str decode(ByteBuffer byteSlice) {
+ byteSlice.limit(byteSlice.limit() - 1);
+ return new Str(charset.decode(byteSlice).toString());
+ }
+ };
+ }
+}
diff --git a/src/main/java/eu/mulk/jgvariant/core/Value.java b/src/main/java/eu/mulk/jgvariant/core/Value.java
new file mode 100644
index 0000000..969f240
--- /dev/null
+++ b/src/main/java/eu/mulk/jgvariant/core/Value.java
@@ -0,0 +1,35 @@
+package eu.mulk.jgvariant.core;
+
+import java.util.List;
+import java.util.Optional;
+
+/** A value representable by the GVariant serialization format. */
+public sealed interface Value {
+
+ // Composite types
+ record Array<T extends Value>(List<T> values) implements Value {}
+
+ record Maybe<T extends Value>(Optional<T> value) implements Value {}
+
+ record Structure<T extends Record>(T values) implements Value {}
+
+ record Variant(Class<? extends Value> type, Value value) implements Value {}
+
+ // Primitive types
+ record Bool(boolean value) implements Value {
+ static Bool TRUE = new Bool(true);
+ static Bool FALSE = new Bool(false);
+ }
+
+ record Int8(byte value) implements Value {}
+
+ record Int16(short value) implements Value {}
+
+ record Int32(int value) implements Value {}
+
+ record Int64(long value) implements Value {}
+
+ record Float64(double value) implements Value {}
+
+ record Str(String value) implements Value {}
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000..8880c0b
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,7 @@
+module eu.mulk.jgvariant.core {
+ requires java.base;
+ requires com.google.errorprone.annotations;
+ requires org.jetbrains.annotations;
+
+ exports eu.mulk.jgvariant.core;
+}
diff --git a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
new file mode 100644
index 0000000..4deb8bc
--- /dev/null
+++ b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
@@ -0,0 +1,278 @@
+package eu.mulk.jgvariant.core;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import eu.mulk.jgvariant.core.Value.Array;
+import eu.mulk.jgvariant.core.Value.Bool;
+import eu.mulk.jgvariant.core.Value.Int32;
+import eu.mulk.jgvariant.core.Value.Int8;
+import eu.mulk.jgvariant.core.Value.Maybe;
+import eu.mulk.jgvariant.core.Value.Str;
+import eu.mulk.jgvariant.core.Value.Structure;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests based on the examples given in <a
+ * href="https://people.gnome.org/~desrt/gvariant-serialisation.pdf">~desrt/gvariant-serialisation.pdf</a>.
+ */
+class DecoderTest {
+
+ @Test
+ void testString() {
+ var data = new byte[] {0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00};
+ var decoder = Decoder.ofStr(UTF_8);
+ assertEquals(new Str("hello world"), decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testMaybe() {
+ var data =
+ new byte[] {0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00, 0x00};
+ var decoder = Decoder.ofMaybe(Decoder.ofStr(UTF_8));
+ assertEquals(
+ new Maybe<>(Optional.of(new Str("hello world"))), decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testBooleanArray() {
+ var data = new byte[] {0x01, 0x00, 0x00, 0x01, 0x01};
+ var decoder = Decoder.ofArray(Decoder.ofBoolean());
+ assertEquals(
+ new Array<>(List.of(Bool.TRUE, Bool.FALSE, Bool.FALSE, Bool.TRUE, Bool.TRUE)),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testStructure() {
+ var data =
+ new byte[] {
+ 0x66, 0x6F, 0x6F, 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0x04
+ };
+
+ record TestRecord(Str s, Int32 i) {}
+
+ var decoder = Decoder.ofStructure(TestRecord.class, Decoder.ofStr(UTF_8), Decoder.ofInt32());
+ assertEquals(
+ new Structure<>(new TestRecord(new Str("foo"), new Int32(-1))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testComplexStructureArray() {
+ var data =
+ new byte[] {
+ 0x68,
+ 0x69,
+ 0x00,
+ 0x00,
+ (byte) 0xfe,
+ (byte) 0xff,
+ (byte) 0xff,
+ (byte) 0xff,
+ 0x03,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x62,
+ 0x79,
+ 0x65,
+ 0x00,
+ (byte) 0xff,
+ (byte) 0xff,
+ (byte) 0xff,
+ (byte) 0xff,
+ 0x04,
+ 0x09,
+ 0x15
+ };
+
+ record TestRecord(Str s, Int32 i) {}
+
+ var decoder =
+ Decoder.ofArray(
+ Decoder.ofStructure(
+ TestRecord.class,
+ Decoder.ofStr(UTF_8),
+ Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN)));
+ assertEquals(
+ new Array<>(
+ List.of(
+ new Structure<>(new TestRecord(new Str("hi"), new Int32(-2))),
+ new Structure<>(new TestRecord(new Str("bye"), new Int32(-1))))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testStringArray() {
+ var data =
+ new byte[] {
+ 0x69, 0x00, 0x63, 0x61, 0x6E, 0x00, 0x68, 0x61, 0x73, 0x00, 0x73, 0x74, 0x72, 0x69, 0x6E,
+ 0x67, 0x73, 0x3F, 0x00, 0x02, 0x06, 0x0a, 0x13
+ };
+ var decoder = Decoder.ofArray(Decoder.ofStr(UTF_8));
+ assertEquals(
+ new Array<>(List.of(new Str("i"), new Str("can"), new Str("has"), new Str("strings?"))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testNestedStructure() {
+ var data =
+ new byte[] {
+ 0x69, 0x63, 0x61, 0x6E, 0x00, 0x68, 0x61, 0x73, 0x00, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67,
+ 0x73, 0x3F, 0x00, 0x04, 0x0d, 0x05
+ };
+
+ record TestChild(Int8 b, Str s) {}
+ record TestParent(Structure<TestChild> tc, Array<Str> as) {}
+
+ var decoder =
+ Decoder.ofStructure(
+ TestParent.class,
+ Decoder.ofStructure(TestChild.class, Decoder.ofInt8(), Decoder.ofStr(UTF_8)),
+ Decoder.ofArray(Decoder.ofStr(UTF_8)));
+
+ assertEquals(
+ new Structure<>(
+ new TestParent(
+ new Structure<>(new TestChild(new Int8((byte) 0x69), new Str("can"))),
+ new Array<>(List.of(new Str("has"), new Str("strings?"))))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testSimpleStructure() {
+ var data = new byte[] {0x60, 0x70};
+
+ record TestRecord(Int8 b1, Int8 b2) {}
+
+ var decoder =
+ Decoder.ofStructure(
+ TestRecord.class,
+ Decoder.ofInt8().withByteOrder(LITTLE_ENDIAN),
+ Decoder.ofInt8().withByteOrder(LITTLE_ENDIAN));
+
+ assertEquals(
+ new Structure<>(new TestRecord(new Int8((byte) 0x60), new Int8((byte) 0x70))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testPaddedStructureRight() {
+ var data = new byte[] {0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00};
+
+ record TestRecord(Int32 b1, Int8 b2) {}
+
+ var decoder =
+ Decoder.ofStructure(
+ TestRecord.class,
+ Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN),
+ Decoder.ofInt8().withByteOrder(LITTLE_ENDIAN));
+
+ assertEquals(
+ new Structure<>(new TestRecord(new Int32(0x60), new Int8((byte) 0x70))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testPaddedStructureLeft() {
+ var data = new byte[] {0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00};
+
+ record TestRecord(Int8 b1, Int32 b2) {}
+
+ var decoder =
+ Decoder.ofStructure(
+ TestRecord.class,
+ Decoder.ofInt8().withByteOrder(LITTLE_ENDIAN),
+ Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN));
+
+ assertEquals(
+ new Structure<>(new TestRecord(new Int8((byte) 0x60), new Int32(0x70))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testSimpleStructureArray() {
+ var data =
+ new byte[] {
+ 0x60,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x70,
+ 0x00,
+ 0x00,
+ 0x00,
+ (byte) 0x88,
+ 0x02,
+ 0x00,
+ 0x00,
+ (byte) 0xF7,
+ 0x00,
+ 0x00,
+ 0x00
+ };
+
+ record TestRecord(Int32 b1, Int8 b2) {}
+
+ var decoder =
+ Decoder.ofArray(
+ Decoder.ofStructure(
+ TestRecord.class,
+ Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN),
+ Decoder.ofInt8().withByteOrder(LITTLE_ENDIAN)));
+
+ assertEquals(
+ new Array<>(
+ List.of(
+ new Structure<>(new TestRecord(new Int32(96), new Int8((byte) 0x70))),
+ new Structure<>(new TestRecord(new Int32(648), new Int8((byte) 0xf7))))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testByteArray() {
+ var data = new byte[] {0x04, 0x05, 0x06, 0x07};
+
+ var decoder = Decoder.ofArray(Decoder.ofInt8());
+
+ assertEquals(
+ new Array<>(
+ List.of(
+ new Int8((byte) 0x04),
+ new Int8((byte) 0x05),
+ new Int8((byte) 0x06),
+ new Int8((byte) 0x07))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testIntegerArray() {
+ var data = new byte[] {0x04, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00};
+
+ var decoder = Decoder.ofArray(Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN));
+
+ assertEquals(
+ new Array<>(List.of(new Int32(4), new Int32(258))), decoder.decode(ByteBuffer.wrap(data)));
+ }
+
+ @Test
+ void testDictionaryEntry() {
+ var data =
+ new byte[] {0x61, 0x20, 0x6B, 0x65, 0x79, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x06};
+
+ record TestEntry(Str key, Int32 value) {}
+
+ var decoder =
+ Decoder.ofStructure(
+ TestEntry.class, Decoder.ofStr(UTF_8), Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN));
+ assertEquals(
+ new Structure<>(new TestEntry(new Str("a key"), new Int32(514))),
+ decoder.decode(ByteBuffer.wrap(data)));
+ }
+}