diff options
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Decoder.java | 161 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Variant.java | 102 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/package-info.java | 13 | ||||
-rw-r--r-- | src/main/java/module-info.java | 6 | ||||
-rw-r--r-- | src/test/java/eu/mulk/jgvariant/core/DecoderTest.java | 19 |
6 files changed, 155 insertions, 152 deletions
@@ -5,11 +5,7 @@ This library provides a [GVariant][] parser in pure Java. ## Overview -The two foundational classes are `Value` and `Decoder`. - -`Value` is a sum type (sealed interface) that represents a -[GVariant][] value. Its subtypes represent the different types of -values that [GVariant][] supports. +The foundational class is `Decoder`. Instances of `Decoder` read a given concrete subtype of `Value` from a [ByteBuffer][]. The class also contains factory methods to create diff --git a/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/src/main/java/eu/mulk/jgvariant/core/Decoder.java index bb479ff..9833998 100644 --- a/src/main/java/eu/mulk/jgvariant/core/Decoder.java +++ b/src/main/java/eu/mulk/jgvariant/core/Decoder.java @@ -7,6 +7,7 @@ import java.lang.reflect.RecordComponent; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -14,7 +15,7 @@ import java.util.Optional; import org.jetbrains.annotations.Nullable; /** - * Type class for decodable {@link Variant} types. + * 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. @@ -111,7 +112,7 @@ public abstract class Decoder<T> { } /** - * Creates a {@link Decoder} for a {@code Structure} type. + * 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. @@ -124,11 +125,38 @@ public abstract class Decoder<T> { } /** - * Creates a {@link Decoder} for the {@link Variant} type. + * 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<Variant> ofVariant() { + public static Decoder<Object[]> ofStructure(Decoder<?>... componentDecoders) { + return new TupleDecoder(componentDecoders); + } + + /** + * Creates a {@link Decoder} for the {@code Variant} type. + * + * <p>The returned {@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>{@link Object[]} (a GVariant structure) + * </ul> + * + * @return a new {@link Decoder}. + */ + public static Decoder<Object> ofVariant() { return new VariantDecoder(); } @@ -142,7 +170,7 @@ public abstract class Decoder<T> { } /** - * Creates a {@link Decoder} for the 8-bit {@ode byte} type. + * 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. @@ -314,21 +342,56 @@ public abstract class Decoder<T> { private static class StructureDecoder<U extends Record> extends Decoder<U> { - private final RecordComponent[] recordComponents; private final Class<U> recordType; - private final Decoder<?>[] componentDecoders; + 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.recordComponents = recordComponents; 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; } @@ -358,10 +421,10 @@ public abstract class Decoder<T> { } @Override - public U decode(ByteBuffer byteSlice) { + public Object[] decode(ByteBuffer byteSlice) { int framingOffsetSize = byteCount(byteSlice.limit()); - var recordConstructorArguments = new Object[recordComponents.length]; + var objects = new Object[componentDecoders.length]; int position = 0; int framingOffsetIndex = 0; @@ -371,14 +434,14 @@ public abstract class Decoder<T> { var fixedComponentSize = componentDecoder.fixedSize(); if (fixedComponentSize != null) { - recordConstructorArguments[componentIndex] = + objects[componentIndex] = componentDecoder.decode(byteSlice.slice(position, fixedComponentSize)); position += fixedComponentSize; } else { - if (componentIndex == recordComponents.length - 1) { + if (componentIndex == componentDecoders.length - 1) { // The last component never has a framing offset. int endPosition = byteSlice.limit() - framingOffsetIndex * framingOffsetSize; - recordConstructorArguments[componentIndex] = + objects[componentIndex] = componentDecoder.decode(byteSlice.slice(position, endPosition - position)); position = endPosition; } else { @@ -387,7 +450,7 @@ public abstract class Decoder<T> { byteSlice.slice( byteSlice.limit() - (1 + framingOffsetIndex) * framingOffsetSize, framingOffsetSize)); - recordConstructorArguments[componentIndex] = + objects[componentIndex] = componentDecoder.decode(byteSlice.slice(position, framingOffset - position)); position = framingOffset; ++framingOffsetIndex; @@ -397,23 +460,11 @@ public abstract class Decoder<T> { ++componentIndex; } - 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); - } + return objects; } } - private static class VariantDecoder extends Decoder<Variant> { + private static class VariantDecoder extends Decoder<Object> { @Override public byte alignment() { @@ -427,9 +478,55 @@ public abstract class Decoder<T> { } @Override - public Variant decode(ByteBuffer byteSlice) { - // TODO - throw new UnsupportedOperationException("not implemented"); + public Object decode(ByteBuffer byteSlice) { + for (int i = byteSlice.limit() - 1; i >= 0; --i) { + if (byteSlice.get(i) != 0) { + continue; + } + + var data = byteSlice.slice(0, i); + var signature = byteSlice.slice(i + 1, byteSlice.limit() - (i + 1)); + + Decoder<?> decoder = parseSignature(signature); + return decoder.decode(data); + } + + throw new IllegalArgumentException("variant signature not found"); + } + + private static Decoder<?> parseSignature(ByteBuffer signature) { + 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 IllegalArgumentException( + String.format("encountered unknown signature byte '%c'", c)); + }; + } + + private static List<Decoder<?>> parseTupleTypes(ByteBuffer signature) { + 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/src/main/java/eu/mulk/jgvariant/core/Variant.java b/src/main/java/eu/mulk/jgvariant/core/Variant.java deleted file mode 100644 index 05e28d5..0000000 --- a/src/main/java/eu/mulk/jgvariant/core/Variant.java +++ /dev/null @@ -1,102 +0,0 @@ -package eu.mulk.jgvariant.core; - -import java.util.List; -import java.util.Optional; - -/** - * A value representable by the <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> - * serialization format, tagged with its type. - * - * <p>{@link Variant} is a sum type (sealed interface) that represents a GVariant value. Its - * subtypes represent the different types of values that GVariant supports. - * - * @see Decoder#ofVariant() - */ -public sealed interface Variant { - - /** - * A homogeneous sequence of GVariant values. - * - * <p>Arrays of fixed width (i.e. of values of fixed size) are represented in a similar way to - * plain C arrays. Arrays of variable width require additional space for padding and framing. - * - * <p>Heterogeneous sequences are represented by {@code Array<Variant>}. - * - * @param <T> the type of the elements of the array. - * @see Decoder#ofArray - */ - record Array<T>(List<T> values) implements Variant {} - - /** - * A value that is either present or absent. - * - * @param <T> the contained type. - * @see Decoder#ofMaybe - */ - record Maybe<T>(Optional<T> value) implements Variant {} - - /** - * A tuple of values of fixed types. - * - * <p>GVariant structures are represented as {@link Record} types. For example, a two-element - * structure consisting of a string and an int can be modelled as follows: - * - * <pre>{@code - * record TestRecord(String s, int i) {} - * var testStruct = new Structure<>(new TestRecord("hello", 123); - * }</pre> - * - * @param <T> the {@link Record} type that represents the components of the structure. - * @see Decoder#ofStructure - */ - record Structure<T extends Record>(T values) implements Variant {} - - /** - * Either true or false. - * - * @see Decoder#ofBoolean() - */ - record Bool(boolean value) implements Variant {} - - /** - * A {@code byte}-sized integer. - * - * @see Decoder#ofByte() - */ - record Byte(byte value) implements Variant {} - - /** - * A {@code short}-sized integer. - * - * @see Decoder#ofShort() - */ - record Short(short value) implements Variant {} - - /** - * An {@code int}-sized integer. - * - * @see Decoder#ofInt() - */ - record Int(int value) implements Variant {} - - /** - * A {@code long}-sized integer. - * - * @see Decoder#ofLong() - */ - record Long(long value) implements Variant {} - - /** - * A double-precision floating point number. - * - * @see Decoder#ofDouble() - */ - record Double(double value) implements Variant {} - - /** - * A character string. - * - * @see Decoder#ofString - */ - record String(java.lang.String value) implements Variant {} -} diff --git a/src/main/java/eu/mulk/jgvariant/core/package-info.java b/src/main/java/eu/mulk/jgvariant/core/package-info.java index 1b819c5..1754096 100644 --- a/src/main/java/eu/mulk/jgvariant/core/package-info.java +++ b/src/main/java/eu/mulk/jgvariant/core/package-info.java @@ -1,14 +1,9 @@ /** - * Provides {@link eu.mulk.jgvariant.core.Variant} and {@link eu.mulk.jgvariant.core.Decoder}, the - * foundational classes for <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> - * parsing. + * 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>{@link eu.mulk.jgvariant.core.Variant} is a sum type (sealed interface) that represents a - * GVariant value. Its subtypes represent the different types of values that GVariant supports. - * - * <p>Instances of {@link eu.mulk.jgvariant.core.Decoder} read a given concrete subtype of {@link - * eu.mulk.jgvariant.core.Variant} from a {@link java.nio.ByteBuffer}. The class also contains - * factory methods to create those instances. + * <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> * diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index af28413..39e91b8 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,11 +1,9 @@ -import eu.mulk.jgvariant.core.Variant; - /** * Provides a parser for the <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> * serialization format. * - * <p>The {@link eu.mulk.jgvariant.core} package contains the {@link Variant} and {@link - * eu.mulk.jgvariant.core.Decoder} types. which form the foundation of this library. + * <p>The {@link eu.mulk.jgvariant.core} package contains the {@link eu.mulk.jgvariant.core.Decoder} + * type. which forms the foundation of this library. */ module eu.mulk.jgvariant.core { requires com.google.errorprone.annotations; diff --git a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java index d37f6a2..0e16973 100644 --- a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java +++ b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java @@ -2,6 +2,8 @@ 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.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.ByteBuffer; @@ -126,6 +128,23 @@ class DecoderTest { } @Test + void testNestedStructureVariant() { + 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, 0x00, 0x28, 0x28, 0x79, 0x73, 0x29, 0x61, 0x73, 0x29 + }; + + var decoder = Decoder.ofVariant(); + var result = (Object[]) decoder.decode(ByteBuffer.wrap(data)); + + assertAll( + () -> assertEquals(2, result.length), + () -> assertArrayEquals(new Object[] {(byte) 0x69, "can"}, (Object[]) result[0]), + () -> assertEquals(List.of("has", "strings?"), result[1])); + } + + @Test void testSimpleStructure() { var data = new byte[] {0x60, 0x70}; |