aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-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
3 files changed, 460 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;
+}