From 55c34814bc7e6749a1530e7b36b51e0bc6358df3 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Tue, 14 Dec 2021 21:51:10 +0100 Subject: Remove Variant class, parse variants into Object. Change-Id: I9b4b3079aea42b74f6fcf6341305b6fded9234f4 --- src/main/java/eu/mulk/jgvariant/core/Decoder.java | 161 +++++++++++++++++---- src/main/java/eu/mulk/jgvariant/core/Variant.java | 102 ------------- .../java/eu/mulk/jgvariant/core/package-info.java | 13 +- src/main/java/module-info.java | 6 +- .../java/eu/mulk/jgvariant/core/DecoderTest.java | 19 +++ 5 files changed, 154 insertions(+), 147 deletions(-) delete mode 100644 src/main/java/eu/mulk/jgvariant/core/Variant.java (limited to 'src') 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. * *

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 { } /** - * 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 { } /** - * Creates a {@link Decoder} for the {@link Variant} type. + * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link List}. * + *

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 ofVariant() { + public static Decoder ofStructure(Decoder... componentDecoders) { + return new TupleDecoder(componentDecoders); + } + + /** + * Creates a {@link Decoder} for the {@code Variant} type. + * + *

The returned {@link Object} can be of one of the following types: + * + *

+ * + * @return a new {@link Decoder}. + */ + public static Decoder ofVariant() { return new VariantDecoder(); } @@ -142,7 +170,7 @@ public abstract class Decoder { } /** - * Creates a {@link Decoder} for the 8-bit {@ode byte} type. + * Creates a {@link Decoder} for the 8-bit {@code byte} type. * *

Note: It is often useful to apply {@link #withByteOrder(ByteOrder)} to the * result of this method. @@ -314,21 +342,56 @@ public abstract class Decoder { private static class StructureDecoder extends Decoder { - private final RecordComponent[] recordComponents; private final Class recordType; - private final Decoder[] componentDecoders; + private final TupleDecoder tupleDecoder; StructureDecoder(Class 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 { + + private final Decoder[] componentDecoders; + + TupleDecoder(Decoder... componentDecoders) { this.componentDecoders = componentDecoders; } @@ -358,10 +421,10 @@ public abstract class Decoder { } @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 { 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 { 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 { ++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 { + private static class VariantDecoder extends Decoder { @Override public byte alignment() { @@ -427,9 +478,55 @@ public abstract class Decoder { } @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> parseTupleTypes(ByteBuffer signature) { + List> 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 GVariant - * serialization format, tagged with its type. - * - *

{@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. - * - *

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. - * - *

Heterogeneous sequences are represented by {@code Array}. - * - * @param the type of the elements of the array. - * @see Decoder#ofArray - */ - record Array(List values) implements Variant {} - - /** - * A value that is either present or absent. - * - * @param the contained type. - * @see Decoder#ofMaybe - */ - record Maybe(Optional value) implements Variant {} - - /** - * A tuple of values of fixed types. - * - *

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: - * - *

{@code
-   * record TestRecord(String s, int i) {}
-   * var testStruct = new Structure<>(new TestRecord("hello", 123);
-   * }
- * - * @param the {@link Record} type that represents the components of the structure. - * @see Decoder#ofStructure - */ - record Structure(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 GVariant - * parsing. + * Provides {@link eu.mulk.jgvariant.core.Decoder}, the foundational class for GVariant parsing. * - *

{@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. - * - *

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. + *

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. * *

Example * 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 GVariant * serialization format. * - *

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. + *

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; @@ -125,6 +127,23 @@ class DecoderTest { decoder.decode(ByteBuffer.wrap(data))); } + @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}; -- cgit v1.2.3