diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Decoder.java | 418 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Value.java | 35 | ||||
-rw-r--r-- | src/main/java/module-info.java | 7 | ||||
-rw-r--r-- | src/test/java/eu/mulk/jgvariant/core/DecoderTest.java | 278 |
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))); + } +} |