aboutsummaryrefslogtreecommitdiff
path: root/jgvariant-core/src/main/java/eu
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2021-12-18 23:38:46 +0100
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2021-12-18 23:38:46 +0100
commit796b19da1b9ef6c1721faa2ddf35100eb01a8a28 (patch)
tree44c4edf40e73b11d09535d011de95be71d0fdec0 /jgvariant-core/src/main/java/eu
parent8bbc9c1a04177dc0bc0082cc59d98d57eead4c1f (diff)
POM: Split into -bom, -core, -parent, -bundle.
Change-Id: I1fd4cc766b60266ef9dcc40e943b45d067dd7b90
Diffstat (limited to 'jgvariant-core/src/main/java/eu')
-rw-r--r--jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java652
-rw-r--r--jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java116
-rw-r--r--jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java30
-rw-r--r--jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java27
4 files changed, 825 insertions, 0 deletions
diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java
new file mode 100644
index 0000000..d2f2403
--- /dev/null
+++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java
@@ -0,0 +1,652 @@
+package eu.mulk.jgvariant.core;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+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.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Type class for decodable types.
+ *
+ * <p>Use the {@code of*} family of constructor methods to acquire a suitable {@link Decoder} for
+ * the type you wish to decode.
+ *
+ * <p><strong>Example</strong>
+ *
+ * <p>To parse a GVariant of type {@code "a(si)"}, which is an array of pairs of {@link String} and
+ * {@code int}, you can use the following code:
+ *
+ * <pre>{@code
+ * record ExampleRecord(String s, int i) {}
+ *
+ * var decoder =
+ * Decoder.ofArray(
+ * Decoder.ofStructure(
+ * ExampleRecord.class,
+ * Decoder.ofString(UTF_8),
+ * Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)));
+ *
+ * byte[] bytes = ...;
+ * List<ExampleRecord> example = decoder.decode(ByteBuffer.wrap(bytes));
+ * }</pre>
+ *
+ * @param <T> the type that the {@link Decoder} can decode.
+ */
+@SuppressWarnings("java:S1610")
+@API(status = Status.EXPERIMENTAL)
+public abstract class Decoder<T> {
+
+ private Decoder() {}
+
+ /**
+ * Decodes a {@link ByteBuffer} holding a serialized GVariant into a value of type {@code T}.
+ *
+ * <p><strong>Note:</strong> Due to the way the GVariant serialization format works, it is
+ * important that the start and end boundaries of the passed byte slice correspond to the actual
+ * start and end of the serialized value. The format does generally not allow for the dynamic
+ * discovery of the end of the data structure.
+ *
+ * @param byteSlice a byte slice holding a serialized GVariant.
+ * @return the deserialized value.
+ * @throws java.nio.BufferUnderflowException if the byte buffer is shorter than the requested
+ * data.
+ * @throws IllegalArgumentException if the serialized GVariant is ill-formed
+ */
+ public abstract T decode(ByteBuffer byteSlice);
+
+ abstract byte alignment();
+
+ @Nullable
+ abstract Integer fixedSize();
+
+ final boolean hasFixedSize() {
+ return fixedSize() != null;
+ }
+
+ /**
+ * Switches the input {@link ByteBuffer} to a given {@link ByteOrder} before reading from it.
+ *
+ * @param byteOrder the byte order to use.
+ * @return a new, decorated {@link Decoder}.
+ */
+ 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);
+ }
+ };
+ }
+
+ /**
+ * Creates a {@link Decoder} for an {@code Array} type.
+ *
+ * @param elementDecoder a {@link Decoder} for the elements of the array.
+ * @param <U> the element type.
+ * @return a new {@link Decoder}.
+ */
+ public static <U> Decoder<List<U>> ofArray(Decoder<U> elementDecoder) {
+ return new ArrayDecoder<>(elementDecoder);
+ }
+
+ /**
+ * Creates a {@link Decoder} for a {@code Maybe} type.
+ *
+ * @param elementDecoder a {@link Decoder} for the contained element.
+ * @param <U> the element type.
+ * @return a new {@link Decoder}.
+ */
+ public static <U> Decoder<Optional<U>> ofMaybe(Decoder<U> elementDecoder) {
+ return new MaybeDecoder<>(elementDecoder);
+ }
+
+ /**
+ * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link Record}.
+ *
+ * @param recordType the {@link Record} type that represents the components of the structure.
+ * @param componentDecoders a {@link Decoder} for each component of the structure.
+ * @param <U> the {@link Record} type that represents the components of the structure.
+ * @return a new {@link Decoder}.
+ */
+ public static <U extends Record> Decoder<U> ofStructure(
+ Class<U> recordType, Decoder<?>... componentDecoders) {
+ return new StructureDecoder<>(recordType, componentDecoders);
+ }
+
+ /**
+ * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link List}.
+ *
+ * <p>Prefer {@link #ofStructure(Class, Decoder[])} if possible, which is both more type-safe and
+ * more convenient.
+ *
+ * @param componentDecoders a {@link Decoder} for each component of the structure.
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Object[]> ofStructure(Decoder<?>... componentDecoders) {
+ return new TupleDecoder(componentDecoders);
+ }
+
+ /**
+ * Creates a {@link Decoder} for the {@link Variant} type.
+ *
+ * <p>The contained {@link Object} can be of one of the following types:
+ *
+ * <ul>
+ * <li>{@link Boolean}
+ * <li>{@link Byte}
+ * <li>{@link Short}
+ * <li>{@link Integer}
+ * <li>{@link Long}
+ * <li>{@link String}
+ * <li>{@link Optional} (a GVariant {@code Maybe} type)
+ * <li>{@link List} (a GVariant array)
+ * <li>{@code Object[]} (a GVariant structure)
+ * <li>{@link Variant} (a nested variant)
+ * </ul>
+ *
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Variant> ofVariant() {
+ return new VariantDecoder();
+ }
+
+ /**
+ * Creates a {@link Decoder} for the {@code boolean} type.
+ *
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Boolean> ofBoolean() {
+ return new BooleanDecoder();
+ }
+
+ /**
+ * Creates a {@link Decoder} for the 8-bit {@code byte} type.
+ *
+ * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the
+ * result of this method.
+ *
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Byte> ofByte() {
+ return new ByteDecoder();
+ }
+
+ /**
+ * Creates a {@link Decoder} for the 16-bit {@code short} type.
+ *
+ * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the
+ * result of this method.
+ *
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Short> ofShort() {
+ return new ShortDecoder();
+ }
+
+ /**
+ * Creates a {@link Decoder} for the 32-bit {@code int} type.
+ *
+ * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the
+ * result of this method.
+ *
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Integer> ofInt() {
+ return new IntegerDecoder();
+ }
+
+ /**
+ * Creates a {@link Decoder} for the 64-bit {@code long} type.
+ *
+ * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the
+ * result of this method.
+ *
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Long> ofLong() {
+ return new LongDecoder();
+ }
+
+ /**
+ * Creates a {@link Decoder} for the {@code double} type.
+ *
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<Double> ofDouble() {
+ return new DoubleDecoder();
+ }
+
+ /**
+ * Creates a {@link Decoder} for the {@link String} type.
+ *
+ * <p><strong>Note:</strong> While GVariant does not prescribe any particular encoding, {@link
+ * java.nio.charset.StandardCharsets#UTF_8} is the most common choice.
+ *
+ * @param charset the {@link Charset} the string is encoded in.
+ * @return a new {@link Decoder}.
+ */
+ public static Decoder<String> ofString(Charset charset) {
+ return new StringDecoder(charset);
+ }
+
+ 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;
+ }
+
+ private static class ArrayDecoder<U> extends Decoder<List<U>> {
+
+ private final Decoder<U> elementDecoder;
+
+ ArrayDecoder(Decoder<U> elementDecoder) {
+ this.elementDecoder = elementDecoder;
+ }
+
+ @Override
+ public byte alignment() {
+ return elementDecoder.alignment();
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public List<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 elements;
+ }
+ }
+
+ private static class MaybeDecoder<U> extends Decoder<Optional<U>> {
+
+ private final Decoder<U> elementDecoder;
+
+ MaybeDecoder(Decoder<U> elementDecoder) {
+ this.elementDecoder = elementDecoder;
+ }
+
+ @Override
+ public byte alignment() {
+ return elementDecoder.alignment();
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public Optional<U> decode(ByteBuffer byteSlice) {
+ if (!byteSlice.hasRemaining()) {
+ return Optional.empty();
+ } else {
+ if (!elementDecoder.hasFixedSize()) {
+ // Remove trailing zero byte.
+ byteSlice.limit(byteSlice.limit() - 1);
+ }
+
+ return Optional.of(elementDecoder.decode(byteSlice));
+ }
+ }
+ }
+
+ private static class StructureDecoder<U extends Record> extends Decoder<U> {
+
+ private final Class<U> recordType;
+ private final TupleDecoder tupleDecoder;
+
+ StructureDecoder(Class<U> recordType, Decoder<?>... 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));
+ }
+
+ this.recordType = recordType;
+ this.tupleDecoder = new TupleDecoder(componentDecoders);
+ }
+
+ @Override
+ public byte alignment() {
+ return tupleDecoder.alignment();
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return tupleDecoder.fixedSize();
+ }
+
+ @Override
+ public U decode(ByteBuffer byteSlice) {
+ Object[] recordConstructorArguments = tupleDecoder.decode(byteSlice);
+
+ try {
+ var recordComponentTypes =
+ Arrays.stream(recordType.getRecordComponents())
+ .map(RecordComponent::getType)
+ .toArray(Class<?>[]::new);
+ var recordConstructor = recordType.getDeclaredConstructor(recordComponentTypes);
+ return recordConstructor.newInstance(recordConstructorArguments);
+ } catch (NoSuchMethodException
+ | InstantiationException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ private static class TupleDecoder extends Decoder<Object[]> {
+
+ private final Decoder<?>[] componentDecoders;
+
+ TupleDecoder(Decoder<?>... componentDecoders) {
+ this.componentDecoders = componentDecoders;
+ }
+
+ @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 Object[] decode(ByteBuffer byteSlice) {
+ int framingOffsetSize = byteCount(byteSlice.limit());
+
+ var objects = new Object[componentDecoders.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) {
+ objects[componentIndex] =
+ componentDecoder.decode(byteSlice.slice(position, fixedComponentSize));
+ position += fixedComponentSize;
+ } else {
+ if (componentIndex == componentDecoders.length - 1) {
+ // The last component never has a framing offset.
+ int endPosition = byteSlice.limit() - framingOffsetIndex * framingOffsetSize;
+ objects[componentIndex] =
+ componentDecoder.decode(byteSlice.slice(position, endPosition - position));
+ position = endPosition;
+ } else {
+ int framingOffset =
+ getIntN(
+ byteSlice.slice(
+ byteSlice.limit() - (1 + framingOffsetIndex) * framingOffsetSize,
+ framingOffsetSize));
+ objects[componentIndex] =
+ componentDecoder.decode(byteSlice.slice(position, framingOffset - position));
+ position = framingOffset;
+ ++framingOffsetIndex;
+ }
+ }
+
+ ++componentIndex;
+ }
+
+ return objects;
+ }
+ }
+
+ private static class VariantDecoder extends Decoder<Variant> {
+
+ @Override
+ public byte alignment() {
+ return 8;
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public Variant decode(ByteBuffer byteSlice) {
+ for (int i = byteSlice.limit() - 1; i >= 0; --i) {
+ if (byteSlice.get(i) != 0) {
+ continue;
+ }
+
+ var dataBytes = byteSlice.slice(0, i);
+ var signatureBytes = byteSlice.slice(i + 1, byteSlice.limit() - (i + 1));
+
+ Signature signature;
+ try {
+ signature = Signature.parse(signatureBytes);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ return new Variant(signature, signature.decoder().decode(dataBytes));
+ }
+
+ throw new IllegalArgumentException("variant signature not found");
+ }
+ }
+
+ private static class BooleanDecoder extends Decoder<Boolean> {
+
+ @Override
+ public byte alignment() {
+ return 1;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 1;
+ }
+
+ @Override
+ public Boolean decode(ByteBuffer byteSlice) {
+ return byteSlice.get() != 0;
+ }
+ }
+
+ private static class ByteDecoder extends Decoder<Byte> {
+
+ @Override
+ public byte alignment() {
+ return 1;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 1;
+ }
+
+ @Override
+ public Byte decode(ByteBuffer byteSlice) {
+ return byteSlice.get();
+ }
+ }
+
+ private static class ShortDecoder extends Decoder<Short> {
+
+ @Override
+ public byte alignment() {
+ return 2;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 2;
+ }
+
+ @Override
+ public Short decode(ByteBuffer byteSlice) {
+ return byteSlice.getShort();
+ }
+ }
+
+ private static class IntegerDecoder extends Decoder<Integer> {
+
+ @Override
+ public byte alignment() {
+ return 4;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 4;
+ }
+
+ @Override
+ public Integer decode(ByteBuffer byteSlice) {
+ return byteSlice.getInt();
+ }
+ }
+
+ private static class LongDecoder extends Decoder<Long> {
+
+ @Override
+ public byte alignment() {
+ return 8;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 8;
+ }
+
+ @Override
+ public Long decode(ByteBuffer byteSlice) {
+ return byteSlice.getLong();
+ }
+ }
+
+ private static class DoubleDecoder extends Decoder<Double> {
+
+ @Override
+ public byte alignment() {
+ return 8;
+ }
+
+ @Override
+ public Integer fixedSize() {
+ return 8;
+ }
+
+ @Override
+ public Double decode(ByteBuffer byteSlice) {
+ return byteSlice.getDouble();
+ }
+ }
+
+ private static class StringDecoder extends Decoder<String> {
+
+ private final Charset charset;
+
+ public StringDecoder(Charset charset) {
+ this.charset = charset;
+ }
+
+ @Override
+ public byte alignment() {
+ return 1;
+ }
+
+ @Override
+ @Nullable
+ Integer fixedSize() {
+ return null;
+ }
+
+ @Override
+ public String decode(ByteBuffer byteSlice) {
+ byteSlice.limit(byteSlice.limit() - 1);
+ return charset.decode(byteSlice).toString();
+ }
+ }
+}
diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java
new file mode 100644
index 0000000..d9de5f1
--- /dev/null
+++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java
@@ -0,0 +1,116 @@
+package eu.mulk.jgvariant.core;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * A GVariant signature string.
+ *
+ * <p>Describes a type in the GVariant type system. The type can be arbitrarily complex.
+ *
+ * <p><strong>Examples</strong>
+ *
+ * <dl>
+ * <dt>{@code "i"}
+ * <dd>a single 32-bit integer
+ * <dt>{@code "ai"}
+ * <dd>an array of 32-bit integers
+ * <dt>{@code "(bbb(sai))"}
+ * <dd>a record consisting of three booleans and a nested record, which consists of a string and
+ * an array of 32-bit integers
+ * </dl>
+ */
+@API(status = Status.STABLE)
+public final class Signature {
+
+ private final String signatureString;
+ private final Decoder<?> decoder;
+
+ Signature(ByteBuffer signatureBytes) throws ParseException {
+ this.decoder = parseSignature(signatureBytes);
+
+ signatureBytes.rewind();
+ this.signatureString = StandardCharsets.US_ASCII.decode(signatureBytes).toString();
+ }
+
+ static Signature parse(ByteBuffer signatureBytes) throws ParseException {
+ return new Signature(signatureBytes);
+ }
+
+ static Signature parse(String signatureString) throws ParseException {
+ var signatureBytes = ByteBuffer.wrap(signatureString.getBytes(StandardCharsets.US_ASCII));
+ return parse(signatureBytes);
+ }
+
+ /**
+ * Returns a {@link Decoder} that can decode values conforming to this signature.
+ *
+ * @return a {@link Decoder} for this signature
+ */
+ @SuppressWarnings("unchecked")
+ Decoder<Object> decoder() {
+ return (Decoder<Object>) decoder;
+ }
+
+ /**
+ * Returns the signature formatted as a GVariant signature string.
+ *
+ * @return a GVariant signature string.
+ */
+ @Override
+ public String toString() {
+ return signatureString;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof Signature signature)
+ && Objects.equals(signatureString, signature.signatureString);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(signatureString);
+ }
+
+ private static Decoder<?> parseSignature(ByteBuffer signature) throws ParseException {
+ char c = (char) signature.get();
+ return switch (c) {
+ case 'b' -> Decoder.ofBoolean();
+ case 'y' -> Decoder.ofByte();
+ case 'n', 'q' -> Decoder.ofShort();
+ case 'i', 'u' -> Decoder.ofInt();
+ case 'x', 't' -> Decoder.ofLong();
+ case 'd' -> Decoder.ofDouble();
+ case 's', 'o', 'g' -> Decoder.ofString(StandardCharsets.UTF_8);
+ case 'v' -> Decoder.ofVariant();
+ case 'm' -> Decoder.ofMaybe(parseSignature(signature));
+ case 'a' -> Decoder.ofArray(parseSignature(signature));
+ case '(', '{' -> Decoder.ofStructure(parseTupleTypes(signature).toArray(new Decoder<?>[0]));
+ default -> throw new ParseException(
+ String.format("encountered unknown signature byte '%c'", c), signature.position());
+ };
+ }
+
+ private static List<Decoder<?>> parseTupleTypes(ByteBuffer signature) throws ParseException {
+ List<Decoder<?>> decoders = new ArrayList<>();
+
+ while (true) {
+ char c = (char) signature.get(signature.position());
+ if (c == ')' || c == '}') {
+ signature.get();
+ break;
+ }
+
+ decoders.add(parseSignature(signature));
+ }
+
+ return decoders;
+ }
+}
diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java
new file mode 100644
index 0000000..d1c1049
--- /dev/null
+++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java
@@ -0,0 +1,30 @@
+package eu.mulk.jgvariant.core;
+
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * A dynamically typed GVariant value carrying a {@link Signature} describing its type.
+ *
+ * <p>{@link #value()} can be of one of the following types:
+ *
+ * <ul>
+ * <li>{@link Boolean}
+ * <li>{@link Byte}
+ * <li>{@link Short}
+ * <li>{@link Integer}
+ * <li>{@link Long}
+ * <li>{@link String}
+ * <li>{@link java.util.Optional} (a GVariant {@code Maybe} type)
+ * <li>{@link java.util.List} (a GVariant array)
+ * <li>{@code Object[]} (a GVariant structure)
+ * <li>{@link Variant} (a nested variant)
+ * </ul>
+ *
+ * @param signature the signature describing the type of the value.
+ * @param value the value itself; one of {@link Boolean}, {@link Byte}, {@link Short}, {@link
+ * Integer}, {@link Long}, {@link String}, {@link java.util.Optional}, {@link java.util.List},
+ * {@code Object[]}, {@link Variant}.
+ */
+@API(status = Status.EXPERIMENTAL)
+public record Variant(Signature signature, Object value) {}
diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java
new file mode 100644
index 0000000..1754096
--- /dev/null
+++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * Provides {@link eu.mulk.jgvariant.core.Decoder}, the foundational class for <a
+ * href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> parsing.
+ *
+ * <p>Instances of {@link eu.mulk.jgvariant.core.Decoder} read a given value type from a {@link
+ * java.nio.ByteBuffer}. The class also contains factory methods to create those instances.
+ *
+ * <p><strong>Example</strong>
+ *
+ * <p>To parse a GVariant of type {@code "a(si)"}, which is an array of pairs of {@link
+ * java.lang.String} and {@code int}, you can use the following code:
+ *
+ * <pre>{@code
+ * record ExampleRecord(String s, int i) {}
+ *
+ * var decoder =
+ * Decoder.ofArray(
+ * Decoder.ofStructure(
+ * ExampleRecord.class,
+ * Decoder.ofString(UTF_8),
+ * Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)));
+ *
+ * byte[] bytes = ...;
+ * List<ExampleRecord> example = decoder.decode(ByteBuffer.wrap(bytes));
+ * }</pre>
+ */
+package eu.mulk.jgvariant.core;