diff options
-rw-r--r-- | README.md | 43 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Decoder.java | 104 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Value.java | 83 | ||||
-rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/package-info.java | 32 | ||||
-rw-r--r-- | src/main/java/module-info.java | 8 | ||||
-rw-r--r-- | src/test/java/eu/mulk/jgvariant/core/DecoderTest.java | 2 |
6 files changed, 266 insertions, 6 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..e482857 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# GVariant for Java + +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. + +Instances of `Decoder` read a given concrete subtype of `Value` from a +[ByteBuffer][]. The class also contains factory methods to create +those instances. + +The various subclasses of `Decoder` together implement the [GVariant +serialization][] specification. + + +## Example + +To parse a [GVariant][] value of type `"a(si)"`, which is an array of +pairs of [String][] and `int`, you can use the following code: + + record ExampleRecord(Value.Str s, Value.Int32 i) {} + + var decoder = + Decoder.ofArray( + Decoder.ofStructure( + ExampleRecord.class, + Decoder.ofStr(StandardCharsets.UTF_8), + Decoder.ofInt32().withByteOrder(ByteOrder.LITTLE_ENDIAN))); + + byte[] bytes = ...; + Value.Array<Value.Structure<ExampleRecord>> example = decoder.decode(bytes); + + +[ByteBuffer]: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/ByteBuffer.html +[GVariant]: https://docs.gtk.org/glib/struct.Variant.html +[GVariant serialization]: https://people.gnome.org/~desrt/gvariant-serialisation.pdf +[String]: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html diff --git a/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/src/main/java/eu/mulk/jgvariant/core/Decoder.java index 2ebd0af..c51a723 100644 --- a/src/main/java/eu/mulk/jgvariant/core/Decoder.java +++ b/src/main/java/eu/mulk/jgvariant/core/Decoder.java @@ -30,6 +30,25 @@ import org.jetbrains.annotations.Nullable; * <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(Value.Str s, Value.Int32 i) {} + * + * var decoder = + * Decoder.ofArray( + * Decoder.ofStructure( + * ExampleRecord.class, + * Decoder.ofStr(UTF_8), + * Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN))); + * + * byte[] bytes = ...; + * Value.Array<Value.Structure<ExampleRecord>> example = decoder.decode(ByteBuffer.wrap(bytes)); + * }</pre> + * * @param <T> the type that the {@link Decoder} can decode. */ @SuppressWarnings("java:S1610") @@ -52,6 +71,12 @@ public abstract class Decoder<T extends Value> { 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; @@ -74,6 +99,13 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for an {@link 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 extends Value> Decoder<Array<U>> ofArray(Decoder<U> elementDecoder) { return new Decoder<>() { @Override @@ -138,6 +170,13 @@ public abstract class Decoder<T extends Value> { return n < (1 << 8) ? 1 : n < (1 << 16) ? 2 : 4; } + /** + * Creates a {@link Decoder} for a {@link Maybe} type. + * + * @param elementDecoder a {@link Decoder} for the contained element. + * @param <U> the element type. + * @return a new {@link Decoder}. + */ public static <U extends Value> Decoder<Maybe<U>> ofMaybe(Decoder<U> elementDecoder) { return new Decoder<>() { @Override @@ -167,6 +206,14 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for a {@link Structure} type. + * + * @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}. + */ @SafeVarargs public static <U extends Record> Decoder<Structure<U>> ofStructure( Class<U> recordType, Decoder<? extends Value>... componentDecoders) { @@ -260,6 +307,11 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for the {@link Variant} type. + * + * @return a new {@link Decoder}. + */ public static Decoder<Variant> ofVariant() { return new Decoder<>() { @Override @@ -281,7 +333,12 @@ public abstract class Decoder<T extends Value> { }; } - public static Decoder<Bool> ofBoolean() { + /** + * Creates a {@link Decoder} for the {@link Bool} type. + * + * @return a new {@link Decoder}. + */ + public static Decoder<Bool> ofBool() { return new Decoder<>() { @Override public byte alignment() { @@ -300,6 +357,14 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for the {@link Int8} 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<Int8> ofInt8() { return new Decoder<>() { @Override @@ -319,6 +384,14 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for the {@link Int16} 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<Int16> ofInt16() { return new Decoder<>() { @Override @@ -338,6 +411,14 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for the {@link Int32} 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<Int32> ofInt32() { return new Decoder<>() { @Override @@ -357,6 +438,14 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for the {@link Int64} 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<Int64> ofInt64() { return new Decoder<>() { @Override @@ -376,6 +465,11 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for the {@link Float64} type. + * + * @return a new {@link Decoder}. + */ public static Decoder<Float64> ofFloat64() { return new Decoder<>() { @Override @@ -395,6 +489,14 @@ public abstract class Decoder<T extends Value> { }; } + /** + * Creates a {@link Decoder} for the {@link Str} 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. + * + * @return a new {@link Decoder}. + */ public static Decoder<Str> ofStr(Charset charset) { return new Decoder<>() { @Override diff --git a/src/main/java/eu/mulk/jgvariant/core/Value.java b/src/main/java/eu/mulk/jgvariant/core/Value.java index 969f240..54c0b79 100644 --- a/src/main/java/eu/mulk/jgvariant/core/Value.java +++ b/src/main/java/eu/mulk/jgvariant/core/Value.java @@ -3,33 +3,110 @@ package eu.mulk.jgvariant.core; import java.util.List; import java.util.Optional; -/** A value representable by the GVariant serialization format. */ +/** + * A value representable by the <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> + * serialization format. + * + * <p>{@link Value} is a sum type (sealed interface) that represents a GVariant value. Its subtypes + * represent the different types of values that GVariant supports. + * + * @see Decoder + */ public sealed interface Value { - // Composite types + /** + * 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 extends Value>(List<T> values) implements Value {} + /** + * A value that is either present or absent. + * + * @param <T> the contained type. + * @see Decoder#ofMaybe + */ record Maybe<T extends Value>(Optional<T> value) implements Value {} + /** + * 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(Str s, Int32 i) {} + * var testStruct = new Structure<>(new TestRecord(new Str("hello"), new Int32(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 Value {} + /** + * A dynamically typed box that can hold a single value of any GVariant type. + * + * @see Decoder#ofVariant + */ record Variant(Class<? extends Value> type, Value value) implements Value {} - // Primitive types + /** + * Either true or false. + * + * @see Decoder#ofBool() + */ record Bool(boolean value) implements Value { static Bool TRUE = new Bool(true); static Bool FALSE = new Bool(false); } + /** + * A {@code byte}-sized integer. + * + * @see Decoder#ofInt8() + */ record Int8(byte value) implements Value {} + /** + * A {@code short}-sized integer. + * + * @see Decoder#ofInt16() + */ record Int16(short value) implements Value {} + /** + * An {@code int}-sized integer. + * + * @see Decoder#ofInt32() + */ record Int32(int value) implements Value {} + /** + * A {@code long}-sized integer. + * + * @see Decoder#ofInt64() + */ record Int64(long value) implements Value {} + /** + * A double-precision floating point number. + * + * @see Decoder#ofFloat64() + */ record Float64(double value) implements Value {} + /** + * A character string. + * + * @see Decoder#ofStr + */ record Str(String value) implements Value {} } diff --git a/src/main/java/eu/mulk/jgvariant/core/package-info.java b/src/main/java/eu/mulk/jgvariant/core/package-info.java new file mode 100644 index 0000000..dba7a09 --- /dev/null +++ b/src/main/java/eu/mulk/jgvariant/core/package-info.java @@ -0,0 +1,32 @@ +/** + * Provides {@link eu.mulk.jgvariant.core.Value} and {@link eu.mulk.jgvariant.core.Decoder}, the + * foundational classes for <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> + * parsing. + * + * <p>{@link eu.mulk.jgvariant.core.Value} 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.Value} 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 String} and + * {@code int}, you can use the following code: + * + * <pre>{@code + * record ExampleRecord(Value.Str s, Value.Int32 i) {} + * + * var decoder = + * Decoder.ofArray( + * Decoder.ofStructure( + * ExampleRecord.class, + * Decoder.ofStr(UTF_8), + * Decoder.ofInt32().withByteOrder(LITTLE_ENDIAN))); + * + * byte[] bytes = ...; + * Value.Array<Value.Structure<ExampleRecord>> example = decoder.decode(ByteBuffer.wrap(bytes)); + * }</pre> + */ +package eu.mulk.jgvariant.core; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8880c0b..403cf1f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,11 @@ +/** + * 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 eu.mulk.jgvariant.core.Value} + * and {@link eu.mulk.jgvariant.core.Decoder} types. which form the foundation of this library. + */ module eu.mulk.jgvariant.core { - requires java.base; requires com.google.errorprone.annotations; requires org.jetbrains.annotations; diff --git a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java index 4deb8bc..13cd398 100644 --- a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java +++ b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java @@ -41,7 +41,7 @@ class DecoderTest { @Test void testBooleanArray() { var data = new byte[] {0x01, 0x00, 0x00, 0x01, 0x01}; - var decoder = Decoder.ofArray(Decoder.ofBoolean()); + var decoder = Decoder.ofArray(Decoder.ofBool()); assertEquals( new Array<>(List.of(Bool.TRUE, Bool.FALSE, Bool.FALSE, Bool.TRUE, Bool.TRUE)), decoder.decode(ByteBuffer.wrap(data))); |