diff options
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Decoder.java | 58 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Signature.java | 113 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Variant.java | 25 | ||||
-rw-r--r-- | src/test/java/eu/mulk/jgvariant/core/DecoderTest.java | 23 |
4 files changed, 173 insertions, 46 deletions
diff --git a/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/src/main/java/eu/mulk/jgvariant/core/Decoder.java index 9833998..389ae85 100644 --- a/src/main/java/eu/mulk/jgvariant/core/Decoder.java +++ b/src/main/java/eu/mulk/jgvariant/core/Decoder.java @@ -7,7 +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.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -47,8 +47,11 @@ public abstract class Decoder<T> { private Decoder() {} /** + * Decodes a {@link ByteBuffer} holding a serialized GVariant into a value of type {@code T}. + * * @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); @@ -156,7 +159,7 @@ public abstract class Decoder<T> { * * @return a new {@link Decoder}. */ - public static Decoder<Object> ofVariant() { + public static Decoder<Variant> ofVariant() { return new VariantDecoder(); } @@ -464,7 +467,7 @@ public abstract class Decoder<T> { } } - private static class VariantDecoder extends Decoder<Object> { + private static class VariantDecoder extends Decoder<Variant> { @Override public byte alignment() { @@ -478,55 +481,26 @@ public abstract class Decoder<T> { } @Override - public Object decode(ByteBuffer byteSlice) { + public Variant 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); - } + var dataBytes = byteSlice.slice(0, i); + var signatureBytes = byteSlice.slice(i + 1, byteSlice.limit() - (i + 1)); - 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; + Signature signature; + try { + signature = Signature.parse(signatureBytes); + } catch (ParseException e) { + throw new IllegalArgumentException(e); } - decoders.add(parseSignature(signature)); + return new Variant(signature, signature.decoder().decode(dataBytes)); } - return decoders; + throw new IllegalArgumentException("variant signature not found"); } } diff --git a/src/main/java/eu/mulk/jgvariant/core/Signature.java b/src/main/java/eu/mulk/jgvariant/core/Signature.java new file mode 100644 index 0000000..bb03b94 --- /dev/null +++ b/src/main/java/eu/mulk/jgvariant/core/Signature.java @@ -0,0 +1,113 @@ +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; + +/** + * 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> + */ +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/src/main/java/eu/mulk/jgvariant/core/Variant.java b/src/main/java/eu/mulk/jgvariant/core/Variant.java new file mode 100644 index 0000000..e2b4b68 --- /dev/null +++ b/src/main/java/eu/mulk/jgvariant/core/Variant.java @@ -0,0 +1,25 @@ +package eu.mulk.jgvariant.core; + +/** + * 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>{@link Object[]} (a GVariant structure) + * </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}, + * {@link Object[]}. + */ +public record Variant(Signature signature, Object value) {} diff --git a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java index 3cae863..5cf1a1c 100644 --- a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java +++ b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.ByteBuffer; +import java.text.ParseException; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -138,9 +139,11 @@ class DecoderTest { }; var decoder = Decoder.ofVariant(); - var result = (Object[]) decoder.decode(ByteBuffer.wrap(data)); + var variant = decoder.decode(ByteBuffer.wrap(data)); + var result = (Object[]) variant.value(); assertAll( + () -> assertEquals(Signature.parse("((ys)as)"), variant.signature()), () -> assertEquals(2, result.length), () -> assertArrayEquals(new Object[] {(byte) 0x69, "can"}, (Object[]) result[0]), () -> assertEquals(List.of("has", "strings?"), result[1])); @@ -371,7 +374,7 @@ class DecoderTest { } @Test - void testSimpleVariantRecord() { + void testSimpleVariantRecord() throws ParseException { // signature: "(bynqiuxtdsogvmiai)" var data = new byte[] { @@ -413,10 +416,22 @@ class DecoderTest { "hi", "hi", "hi", - 9, + new Variant(Signature.parse("i"), 9), Optional.of(10), List.of(11, 12) }, - (Object[]) decoder.decode(ByteBuffer.wrap(data))); + (Object[]) decoder.decode(ByteBuffer.wrap(data)).value()); + } + + @Test + void testSignatureString() throws ParseException { + var data = + new byte[] { + 0x28, 0x62, 0x79, 0x6E, 0x71, 0x69, 0x75, 0x78, 0x74, 0x64, 0x73, 0x6F, 0x67, 0x76, 0x6D, + 0x69, 0x61, 0x69, 0x29 + }; + + var signature = Signature.parse(ByteBuffer.wrap(data)); + assertEquals("(bynqiuxtdsogvmiai)", signature.toString()); } } |