From 796b19da1b9ef6c1721faa2ddf35100eb01a8a28 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sat, 18 Dec 2021 23:38:46 +0100 Subject: POM: Split into -bom, -core, -parent, -bundle. Change-Id: I1fd4cc766b60266ef9dcc40e943b45d067dd7b90 --- .gitignore | 1 + README.md | 20 +- jgvariant-bom/pom.xml | 38 ++ jgvariant-core/pom.xml | 56 ++ .../main/java/eu/mulk/jgvariant/core/Decoder.java | 652 +++++++++++++++++++++ .../java/eu/mulk/jgvariant/core/Signature.java | 116 ++++ .../main/java/eu/mulk/jgvariant/core/Variant.java | 30 + .../java/eu/mulk/jgvariant/core/package-info.java | 27 + jgvariant-core/src/main/java/module-info.java | 79 +++ .../java/eu/mulk/jgvariant/core/DecoderTest.java | 437 ++++++++++++++ jgvariant-parent/pom.xml | 287 +++++++++ pom.xml | 280 +-------- src/main/java/eu/mulk/jgvariant/core/Decoder.java | 652 --------------------- .../java/eu/mulk/jgvariant/core/Signature.java | 116 ---- src/main/java/eu/mulk/jgvariant/core/Variant.java | 30 - .../java/eu/mulk/jgvariant/core/package-info.java | 27 - src/main/java/module-info.java | 63 -- .../java/eu/mulk/jgvariant/core/DecoderTest.java | 437 -------------- 18 files changed, 1756 insertions(+), 1592 deletions(-) create mode 100644 jgvariant-bom/pom.xml create mode 100644 jgvariant-core/pom.xml create mode 100644 jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java create mode 100644 jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Signature.java create mode 100644 jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Variant.java create mode 100644 jgvariant-core/src/main/java/eu/mulk/jgvariant/core/package-info.java create mode 100644 jgvariant-core/src/main/java/module-info.java create mode 100644 jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java create mode 100644 jgvariant-parent/pom.xml delete mode 100644 src/main/java/eu/mulk/jgvariant/core/Decoder.java delete mode 100644 src/main/java/eu/mulk/jgvariant/core/Signature.java delete mode 100644 src/main/java/eu/mulk/jgvariant/core/Variant.java delete mode 100644 src/main/java/eu/mulk/jgvariant/core/package-info.java delete mode 100644 src/main/java/module-info.java delete mode 100644 src/test/java/eu/mulk/jgvariant/core/DecoderTest.java diff --git a/.gitignore b/.gitignore index 530c14c..e8b22c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *~ /.idea /target +/jgvariant-*/target diff --git a/README.md b/README.md index 7671374..56afd9e 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,28 @@ pairs of [String][] and `int`, you can use the following code: ... + + ... + + + + eu.mulk.jgvariant + jgvariant-bom + 0.1.4 + pom + import + + + + ... + + ... eu.mulk.jgvariant jgvariant-core - 0.1.3 ... @@ -59,7 +74,8 @@ pairs of [String][] and `int`, you can use the following code: dependencies { ... - implementation("eu.mulk.jgvariant:jgvariant-core:0.1.3") + implementation(platform("eu.mulk.jgvariant:jgvariant-bom:0.1.4") + implementation("eu.mulk.jgvariant:jgvariant-core") ... } diff --git a/jgvariant-bom/pom.xml b/jgvariant-bom/pom.xml new file mode 100644 index 0000000..d994b68 --- /dev/null +++ b/jgvariant-bom/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + 0.1.4-SNAPSHOT + + jgvariant-bom + pom + + JGVariant Bill of Materials + + + Bill of Materials for the JGVariant library. + + + + eu.mulk.jgvariant + jgvariant-parent + 0.1.4-SNAPSHOT + ../jgvariant-parent/pom.xml + + + + + + + eu.mulk.jgvariant + jgvariant-core + ${project.version} + + + + + diff --git a/jgvariant-core/pom.xml b/jgvariant-core/pom.xml new file mode 100644 index 0000000..29f6742 --- /dev/null +++ b/jgvariant-core/pom.xml @@ -0,0 +1,56 @@ + + + + 4.0.0 + + 0.1.4-SNAPSHOT + + jgvariant-core + jar + + JGVariant Core + https://gerrit.benkard.de/plugins/gitiles/jgvariant + + + GVariant serialization and deserialization. + + + + eu.mulk.jgvariant + jgvariant-parent + 0.1.4-SNAPSHOT + ../jgvariant-parent/pom.xml + + + + + + com.google.errorprone + error_prone_annotations + + + org.jetbrains + annotations + + + org.apiguardian + apiguardian-api + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + 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. + * + *

Use the {@code of*} family of constructor methods to acquire a suitable {@link Decoder} for + * the type you wish to decode. + * + *

Example + * + *

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

{@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 example = decoder.decode(ByteBuffer.wrap(bytes));
+ * }
+ * + * @param the type that the {@link Decoder} can decode. + */ +@SuppressWarnings("java:S1610") +@API(status = Status.EXPERIMENTAL) +public abstract class Decoder { + + private Decoder() {} + + /** + * Decodes a {@link ByteBuffer} holding a serialized GVariant into a value of type {@code T}. + * + *

Note: 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 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 the element type. + * @return a new {@link Decoder}. + */ + public static Decoder> ofArray(Decoder elementDecoder) { + return new ArrayDecoder<>(elementDecoder); + } + + /** + * Creates a {@link Decoder} for a {@code Maybe} type. + * + * @param elementDecoder a {@link Decoder} for the contained element. + * @param the element type. + * @return a new {@link Decoder}. + */ + public static Decoder> ofMaybe(Decoder 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 the {@link Record} type that represents the components of the structure. + * @return a new {@link Decoder}. + */ + public static Decoder ofStructure( + Class recordType, Decoder... componentDecoders) { + return new StructureDecoder<>(recordType, componentDecoders); + } + + /** + * 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 ofStructure(Decoder... componentDecoders) { + return new TupleDecoder(componentDecoders); + } + + /** + * Creates a {@link Decoder} for the {@link Variant} type. + * + *

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

    + *
  • {@link Boolean} + *
  • {@link Byte} + *
  • {@link Short} + *
  • {@link Integer} + *
  • {@link Long} + *
  • {@link String} + *
  • {@link Optional} (a GVariant {@code Maybe} type) + *
  • {@link List} (a GVariant array) + *
  • {@code Object[]} (a GVariant structure) + *
  • {@link Variant} (a nested variant) + *
+ * + * @return a new {@link Decoder}. + */ + public static Decoder ofVariant() { + return new VariantDecoder(); + } + + /** + * Creates a {@link Decoder} for the {@code boolean} type. + * + * @return a new {@link Decoder}. + */ + public static Decoder ofBoolean() { + return new BooleanDecoder(); + } + + /** + * 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. + * + * @return a new {@link Decoder}. + */ + public static Decoder ofByte() { + return new ByteDecoder(); + } + + /** + * Creates a {@link Decoder} for the 16-bit {@code short} type. + * + *

Note: It is often useful to apply {@link #withByteOrder(ByteOrder)} to the + * result of this method. + * + * @return a new {@link Decoder}. + */ + public static Decoder ofShort() { + return new ShortDecoder(); + } + + /** + * Creates a {@link Decoder} for the 32-bit {@code int} type. + * + *

Note: It is often useful to apply {@link #withByteOrder(ByteOrder)} to the + * result of this method. + * + * @return a new {@link Decoder}. + */ + public static Decoder ofInt() { + return new IntegerDecoder(); + } + + /** + * Creates a {@link Decoder} for the 64-bit {@code long} type. + * + *

Note: It is often useful to apply {@link #withByteOrder(ByteOrder)} to the + * result of this method. + * + * @return a new {@link Decoder}. + */ + public static Decoder ofLong() { + return new LongDecoder(); + } + + /** + * Creates a {@link Decoder} for the {@code double} type. + * + * @return a new {@link Decoder}. + */ + public static Decoder ofDouble() { + return new DoubleDecoder(); + } + + /** + * Creates a {@link Decoder} for the {@link String} type. + * + *

Note: 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 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 extends Decoder> { + + private final Decoder elementDecoder; + + ArrayDecoder(Decoder elementDecoder) { + this.elementDecoder = elementDecoder; + } + + @Override + public byte alignment() { + return elementDecoder.alignment(); + } + + @Override + @Nullable + Integer fixedSize() { + return null; + } + + @Override + public List decode(ByteBuffer byteSlice) { + List 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 extends Decoder> { + + private final Decoder elementDecoder; + + MaybeDecoder(Decoder elementDecoder) { + this.elementDecoder = elementDecoder; + } + + @Override + public byte alignment() { + return elementDecoder.alignment(); + } + + @Override + @Nullable + Integer fixedSize() { + return null; + } + + @Override + public Optional 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 extends Decoder { + + private final Class recordType; + 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.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; + } + + @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 { + + @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 { + + @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 { + + @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 { + + @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 { + + @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 { + + @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 { + + @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 { + + 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. + * + *

Describes a type in the GVariant type system. The type can be arbitrarily complex. + * + *

Examples + * + *

+ *
{@code "i"} + *
a single 32-bit integer + *
{@code "ai"} + *
an array of 32-bit integers + *
{@code "(bbb(sai))"} + *
a record consisting of three booleans and a nested record, which consists of a string and + * an array of 32-bit integers + *
+ */ +@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 decoder() { + return (Decoder) 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> parseTupleTypes(ByteBuffer signature) throws ParseException { + 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/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. + * + *

{@link #value()} can be of one of the following types: + * + *

    + *
  • {@link Boolean} + *
  • {@link Byte} + *
  • {@link Short} + *
  • {@link Integer} + *
  • {@link Long} + *
  • {@link String} + *
  • {@link java.util.Optional} (a GVariant {@code Maybe} type) + *
  • {@link java.util.List} (a GVariant array) + *
  • {@code Object[]} (a GVariant structure) + *
  • {@link Variant} (a nested variant) + *
+ * + * @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 GVariant parsing. + * + *

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

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

{@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 example = decoder.decode(ByteBuffer.wrap(bytes));
+ * }
+ */ +package eu.mulk.jgvariant.core; diff --git a/jgvariant-core/src/main/java/module-info.java b/jgvariant-core/src/main/java/module-info.java new file mode 100644 index 0000000..a1830f6 --- /dev/null +++ b/jgvariant-core/src/main/java/module-info.java @@ -0,0 +1,79 @@ +/** + * Provides a parser for the GVariant + * serialization format. + * + * + * + *

Overview

+ * + *

The {@link eu.mulk.jgvariant.core} package contains the {@link eu.mulk.jgvariant.core.Decoder} + * type, which contains classes to parse and represent serialized GVariant values. + * + *

Installation

+ * + * + * + *

Usage with Maven

+ * + *
{@code
+ * 
+ *   ...
+ *
+ *   
+ *     ...
+ *
+ *     
+ *       
+ *         eu.mulk.jgvariant
+ *         jgvariant-bom
+ *         0.1.4
+ *         pom
+ *         import
+ *       
+ *     
+ *
+ *     ...
+ *   
+ *
+ *   
+ *     ...
+ *
+ *     
+ *       eu.mulk.jgvariant
+ *       jgvariant-core
+ *     
+ *
+ *     ...
+ *   
+ *
+ *   ...
+ * 
+ * }
+ * + *

Usage with Gradle

+ * + *
{@code
+ * dependencies {
+ *   ...
+ *
+ *   implementation(platform("eu.mulk.jgvariant:jgvariant-bom:0.1.4")
+ *   implementation("eu.mulk.jgvariant:jgvariant-core")
+ *
+ *   ...
+ * }
+ * }
+ */ +module eu.mulk.jgvariant.core { + requires com.google.errorprone.annotations; + requires org.jetbrains.annotations; + requires org.apiguardian.api; + + exports eu.mulk.jgvariant.core; +} diff --git a/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java new file mode 100644 index 0000000..5cf1a1c --- /dev/null +++ b/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java @@ -0,0 +1,437 @@ +package eu.mulk.jgvariant.core; + +import static java.nio.ByteOrder.BIG_ENDIAN; +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 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; + +/** + * Tests based on the examples given in ~desrt/gvariant-serialisation.pdf. + */ +class DecoderTest { + + @Test + void testString() { + var data = new byte[] {0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00}; + var decoder = Decoder.ofString(UTF_8); + assertEquals("hello world", decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testMaybe() { + var data = + new byte[] {0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00, 0x00}; + var decoder = Decoder.ofMaybe(Decoder.ofString(UTF_8)); + assertEquals(Optional.of("hello world"), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testBooleanArray() { + var data = new byte[] {0x01, 0x00, 0x00, 0x01, 0x01}; + var decoder = Decoder.ofArray(Decoder.ofBoolean()); + assertEquals(List.of(true, false, false, true, true), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testStructure() { + var data = + new byte[] { + 0x66, 0x6F, 0x6F, 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0x04 + }; + + record TestRecord(String s, int i) {} + + var decoder = Decoder.ofStructure(TestRecord.class, Decoder.ofString(UTF_8), Decoder.ofInt()); + assertEquals(new TestRecord("foo", -1), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testComplexStructureArray() { + var data = + new byte[] { + 0x68, + 0x69, + 0x00, + 0x00, + (byte) 0xfe, + (byte) 0xff, + (byte) 0xff, + (byte) 0xff, + 0x03, + 0x00, + 0x00, + 0x00, + 0x62, + 0x79, + 0x65, + 0x00, + (byte) 0xff, + (byte) 0xff, + (byte) 0xff, + (byte) 0xff, + 0x04, + 0x09, + 0x15 + }; + + record TestRecord(String s, int i) {} + + var decoder = + Decoder.ofArray( + Decoder.ofStructure( + TestRecord.class, + Decoder.ofString(UTF_8), + Decoder.ofInt().withByteOrder(LITTLE_ENDIAN))); + assertEquals( + List.of(new TestRecord("hi", -2), new TestRecord("bye", -1)), + decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testStringArray() { + var data = + new byte[] { + 0x69, 0x00, 0x63, 0x61, 0x6E, 0x00, 0x68, 0x61, 0x73, 0x00, 0x73, 0x74, 0x72, 0x69, 0x6E, + 0x67, 0x73, 0x3F, 0x00, 0x02, 0x06, 0x0a, 0x13 + }; + var decoder = Decoder.ofArray(Decoder.ofString(UTF_8)); + assertEquals(List.of("i", "can", "has", "strings?"), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testNestedStructure() { + 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 + }; + + record TestChild(byte b, String s) {} + record TestParent(TestChild tc, List as) {} + + var decoder = + Decoder.ofStructure( + TestParent.class, + Decoder.ofStructure(TestChild.class, Decoder.ofByte(), Decoder.ofString(UTF_8)), + Decoder.ofArray(Decoder.ofString(UTF_8))); + + assertEquals( + new TestParent(new TestChild((byte) 0x69, "can"), List.of("has", "strings?")), + 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 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])); + } + + @Test + void testSimpleStructure() { + var data = new byte[] {0x60, 0x70}; + + record TestRecord(byte b1, byte b2) {} + + var decoder = + Decoder.ofStructure( + TestRecord.class, + Decoder.ofByte().withByteOrder(LITTLE_ENDIAN), + Decoder.ofByte().withByteOrder(LITTLE_ENDIAN)); + + assertEquals(new TestRecord((byte) 0x60, (byte) 0x70), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testPaddedStructureRight() { + var data = new byte[] {0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00}; + + record TestRecord(int b1, byte b2) {} + + var decoder = + Decoder.ofStructure( + TestRecord.class, + Decoder.ofInt().withByteOrder(LITTLE_ENDIAN), + Decoder.ofByte().withByteOrder(LITTLE_ENDIAN)); + + assertEquals(new TestRecord(0x60, (byte) 0x70), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testPaddedStructureLeft() { + var data = new byte[] {0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00}; + + record TestRecord(byte b1, int b2) {} + + var decoder = + Decoder.ofStructure( + TestRecord.class, + Decoder.ofByte().withByteOrder(LITTLE_ENDIAN), + Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)); + + assertEquals(new TestRecord((byte) 0x60, 0x70), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testSimpleStructureArray() { + var data = + new byte[] { + 0x60, + 0x00, + 0x00, + 0x00, + 0x70, + 0x00, + 0x00, + 0x00, + (byte) 0x88, + 0x02, + 0x00, + 0x00, + (byte) 0xF7, + 0x00, + 0x00, + 0x00 + }; + + record TestRecord(int b1, byte b2) {} + + var decoder = + Decoder.ofArray( + Decoder.ofStructure( + TestRecord.class, + Decoder.ofInt().withByteOrder(LITTLE_ENDIAN), + Decoder.ofByte().withByteOrder(LITTLE_ENDIAN))); + + assertEquals( + List.of(new TestRecord(96, (byte) 0x70), new TestRecord(648, (byte) 0xf7)), + decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testByteArray() { + var data = new byte[] {0x04, 0x05, 0x06, 0x07}; + + var decoder = Decoder.ofArray(Decoder.ofByte()); + + assertEquals( + List.of((byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07), + decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testIntegerArray() { + var data = new byte[] {0x04, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00}; + + var decoder = Decoder.ofArray(Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)); + + assertEquals(List.of(4, 258), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testDictionaryEntry() { + var data = + new byte[] {0x61, 0x20, 0x6B, 0x65, 0x79, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x06}; + + record TestEntry(String key, int value) {} + + var decoder = + Decoder.ofStructure( + TestEntry.class, Decoder.ofString(UTF_8), Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)); + assertEquals(new TestEntry("a key", 514), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testPaddedPrimitives() { + var data = + new byte[] { + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + record TestRecord(short s, long l, double d) {} + + var decoder = + Decoder.ofStructure( + TestRecord.class, + Decoder.ofShort().withByteOrder(BIG_ENDIAN), + Decoder.ofLong().withByteOrder(LITTLE_ENDIAN), + Decoder.ofDouble()); + assertEquals(new TestRecord((short) 1, 2, 3.25), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testEmbeddedMaybe() { + var data = new byte[] {0x01, 0x01}; + + record TestRecord(Optional set, Optional unset) {} + + var decoder = + Decoder.ofStructure( + TestRecord.class, Decoder.ofMaybe(Decoder.ofByte()), Decoder.ofMaybe(Decoder.ofByte())); + assertEquals( + new TestRecord(Optional.of((byte) 1), Optional.empty()), + decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testRecordComponentMismatch() { + record TestRecord(Optional set) {} + + var maybeDecoder = Decoder.ofMaybe(Decoder.ofByte()); + assertThrows( + IllegalArgumentException.class, + () -> Decoder.ofStructure(TestRecord.class, maybeDecoder, maybeDecoder)); + } + + @Test + void testTrivialRecord() { + var data = new byte[] {0x00}; + + record TestRecord() {} + + var decoder = Decoder.ofStructure(TestRecord.class); + assertEquals(new TestRecord(), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testTwoElementTrivialRecordArray() { + var data = new byte[] {0x00, 0x00}; + + record TestRecord() {} + + var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); + assertEquals( + List.of(new TestRecord(), new TestRecord()), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testSingletonTrivialRecordArray() { + var data = new byte[] {0x00}; + + record TestRecord() {} + + var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); + assertEquals(List.of(new TestRecord()), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testEmptyTrivialRecordArray() { + var data = new byte[] {}; + + record TestRecord() {} + + var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); + assertEquals(List.of(), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testVariantArray() { + var data = new byte[] {}; + + record TestRecord() {} + + var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); + assertEquals(List.of(), decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testInvalidVariantSignature() { + var data = new byte[] {0x00, 0x00, 0x2E}; + + var decoder = Decoder.ofVariant(); + assertThrows(IllegalArgumentException.class, () -> decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testMissingVariantSignature() { + var data = new byte[] {0x01}; + + var decoder = Decoder.ofVariant(); + assertThrows(IllegalArgumentException.class, () -> decoder.decode(ByteBuffer.wrap(data))); + } + + @Test + void testSimpleVariantRecord() throws ParseException { + // signature: "(bynqiuxtdsogvmiai)" + var data = + new byte[] { + 0x01, // b + 0x02, // y + 0x00, 0x03, // n + 0x00, 0x04, // q + 0x00, 0x00, // (padding) + 0x00, 0x00, 0x00, 0x05, // i + 0x00, 0x00, 0x00, 0x06, // u + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, // x + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // t + 0x40, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // d + 0x68, 0x69, 0x00, // s + 0x68, 0x69, 0x00, // o + 0x68, 0x69, 0x00, // g + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // (padding) + 0x00, 0x00, 0x00, 0x09, 0x00, 0x69, // v + 0x00, 0x00, // (padding) + 0x00, 0x00, 0x00, 0x0a, // mi + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0c, // ai + 68, 62, 49, 46, 43, // framing offsets + 0x00, 0x28, 0x62, 0x79, 0x6E, 0x71, 0x69, 0x75, 0x78, 0x74, 0x64, 0x73, 0x6F, 0x67, 0x76, + 0x6D, 0x69, 0x61, 0x69, 0x29 + }; + + var decoder = Decoder.ofVariant(); + assertArrayEquals( + new Object[] { + true, + (byte) 2, + (short) 3, + (short) 4, + (int) 5, + (int) 6, + (long) 7, + (long) 8, + (double) 3.25, + "hi", + "hi", + "hi", + new Variant(Signature.parse("i"), 9), + Optional.of(10), + List.of(11, 12) + }, + (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()); + } +} diff --git a/jgvariant-parent/pom.xml b/jgvariant-parent/pom.xml new file mode 100644 index 0000000..f91339a --- /dev/null +++ b/jgvariant-parent/pom.xml @@ -0,0 +1,287 @@ + + + + 4.0.0 + + 0.1.4-SNAPSHOT + + eu.mulk.jgvariant + jgvariant-parent + pom + + JGVariant Parent + https://gerrit.benkard.de/plugins/gitiles/jgvariant + + + Parent POM of the JGVariant library. + + + + + GNU Lesser General Public License v3.0 or later + https://www.gnu.org/licenses/lgpl-3.0.html + + + + + + Matthias Benkard + code@mulk.eu + Matthias Benkard + https://matthias.benkard.de + + + + + scm:git:https://gerrit.benkard.de/jgvariant + scm:git:ssh://gerrit.benkard.de:29418/jgvariant + https://gerrit.benkard.de/plugins/gitiles/jgvariant + + + + true + 17 + + UTF-8 + UTF-8 + + 3.8.1 + ${surefire-plugin.version} + 3.2.0 + 1.12.0 + 1.5 + 3.2.0 + 2.2.1 + 1.6.8 + 2.17.6 + 3.0.0-M5 + 2.8.1 + + 1.1.2 + 2.10.0 + 1.13.0 + 22.0.0 + 5.8.2 + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + + + + + + com.google.errorprone + error_prone_annotations + ${errorprone.version} + + + org.jetbrains + annotations + ${jetbrains-annotations.version} + + + org.apiguardian + apiguardian-api + ${apiguardian.version} + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + + + + + + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + --add-opens eu.mulk.jgvariant.core/eu.mulk.jgvariant.core=ALL-UNNAMED + + + + + + maven-failsafe-plugin + ${failsafe-plugin.version} + + + --add-opens eu.mulk.jgvariant.core/eu.mulk.jgvariant.core=ALL-UNNAMED + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + true + + -XDcompilePolicy=simple + -Xplugin:ErrorProne + -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED + + + + com.google.errorprone + error_prone_core + ${errorprone.version} + + + + + + + maven-jar-plugin + ${jar-plugin.version} + + + + org.codehaus.mojo + versions-maven-plugin + ${versions-plugin.version} + + + + org.apache.maven.plugins + maven-scm-plugin + ${maven-scm-plugin.version} + + v${project.version} + developerConnection + + + + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-plugin.version} + + + + + java,javax,org,com,de,io,dagger,eu.mulk, + + + ${google-java-format.version} + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-plugin.version} + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + + + + + + + release + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + code@mulk.eu + + + + sign-artifacts + verify + + sign + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 5dac0ed..f891dc9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,280 +6,30 @@ 4.0.0 - eu.mulk.jgvariant 0.1.4-SNAPSHOT - jgvariant-core - jar + jgvariant-bundle + pom - JGVariant - https://gerrit.benkard.de/plugins/gitiles/jgvariant + JGVariant Bundle - GVariant serialization and deserialization. + JGVariant library bundle. - - - GNU Lesser General Public License v3.0 or later - https://www.gnu.org/licenses/lgpl-3.0.html - - + + eu.mulk.jgvariant + jgvariant-parent + 0.1.4-SNAPSHOT + jgvariant-parent/pom.xml + - - - Matthias Benkard - code@mulk.eu - Matthias Benkard - https://matthias.benkard.de - - + + jgvariant-parent - - scm:git:https://gerrit.benkard.de/jgvariant - scm:git:ssh://gerrit.benkard.de:29418/jgvariant - https://gerrit.benkard.de/plugins/gitiles/jgvariant - + jgvariant-core - - true - 17 - - UTF-8 - UTF-8 - - 3.8.1 - ${surefire-plugin.version} - 3.2.0 - 1.12.0 - 1.5 - 3.2.0 - 2.2.1 - 1.6.8 - 2.17.6 - 3.0.0-M5 - 2.8.1 - - 1.1.2 - 2.10.0 - 1.13.0 - 22.0.0 - 5.8.2 - - - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - - - - - com.google.errorprone - error_prone_annotations - ${errorprone.version} - - - org.jetbrains - annotations - ${jetbrains-annotations.version} - - - org.apiguardian - apiguardian-api - ${apiguardian.version} - - - - - org.junit.jupiter - junit-jupiter-engine - ${junit-jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-api - ${junit-jupiter.version} - test - - - - - - - - - - - maven-surefire-plugin - ${surefire-plugin.version} - - - --add-opens eu.mulk.jgvariant.core/eu.mulk.jgvariant.core=ALL-UNNAMED - - - - - - maven-failsafe-plugin - ${failsafe-plugin.version} - - - --add-opens eu.mulk.jgvariant.core/eu.mulk.jgvariant.core=ALL-UNNAMED - - - - - - maven-compiler-plugin - ${compiler-plugin.version} - - true - - -XDcompilePolicy=simple - -Xplugin:ErrorProne - -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED - - - - com.google.errorprone - error_prone_core - ${errorprone.version} - - - - - - - maven-jar-plugin - ${jar-plugin.version} - - - - org.codehaus.mojo - versions-maven-plugin - ${versions-plugin.version} - - - - org.apache.maven.plugins - maven-scm-plugin - ${maven-scm-plugin.version} - - v${project.version} - developerConnection - - - - - - - - - - - com.diffplug.spotless - spotless-maven-plugin - ${spotless-plugin.version} - - - - - java,javax,org,com,de,io,dagger,eu.mulk, - - - ${google-java-format.version} - - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-plugin.version} - true - - ossrh - https://s01.oss.sonatype.org/ - true - - - - - - - - - - release - - - - - - org.apache.maven.plugins - maven-source-plugin - ${maven-source-plugin.version} - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - - attach-javadocs - - jar - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - ${maven-gpg-plugin.version} - - code@mulk.eu - - - - sign-artifacts - verify - - sign - - - - - - - - - - + jgvariant-bom + diff --git a/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/src/main/java/eu/mulk/jgvariant/core/Decoder.java deleted file mode 100644 index d2f2403..0000000 --- a/src/main/java/eu/mulk/jgvariant/core/Decoder.java +++ /dev/null @@ -1,652 +0,0 @@ -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. - * - *

Use the {@code of*} family of constructor methods to acquire a suitable {@link Decoder} for - * the type you wish to decode. - * - *

Example - * - *

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

{@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 example = decoder.decode(ByteBuffer.wrap(bytes));
- * }
- * - * @param the type that the {@link Decoder} can decode. - */ -@SuppressWarnings("java:S1610") -@API(status = Status.EXPERIMENTAL) -public abstract class Decoder { - - private Decoder() {} - - /** - * Decodes a {@link ByteBuffer} holding a serialized GVariant into a value of type {@code T}. - * - *

Note: 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 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 the element type. - * @return a new {@link Decoder}. - */ - public static Decoder> ofArray(Decoder elementDecoder) { - return new ArrayDecoder<>(elementDecoder); - } - - /** - * Creates a {@link Decoder} for a {@code Maybe} type. - * - * @param elementDecoder a {@link Decoder} for the contained element. - * @param the element type. - * @return a new {@link Decoder}. - */ - public static Decoder> ofMaybe(Decoder 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 the {@link Record} type that represents the components of the structure. - * @return a new {@link Decoder}. - */ - public static Decoder ofStructure( - Class recordType, Decoder... componentDecoders) { - return new StructureDecoder<>(recordType, componentDecoders); - } - - /** - * 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 ofStructure(Decoder... componentDecoders) { - return new TupleDecoder(componentDecoders); - } - - /** - * Creates a {@link Decoder} for the {@link Variant} type. - * - *

The contained {@link Object} can be of one of the following types: - * - *

    - *
  • {@link Boolean} - *
  • {@link Byte} - *
  • {@link Short} - *
  • {@link Integer} - *
  • {@link Long} - *
  • {@link String} - *
  • {@link Optional} (a GVariant {@code Maybe} type) - *
  • {@link List} (a GVariant array) - *
  • {@code Object[]} (a GVariant structure) - *
  • {@link Variant} (a nested variant) - *
- * - * @return a new {@link Decoder}. - */ - public static Decoder ofVariant() { - return new VariantDecoder(); - } - - /** - * Creates a {@link Decoder} for the {@code boolean} type. - * - * @return a new {@link Decoder}. - */ - public static Decoder ofBoolean() { - return new BooleanDecoder(); - } - - /** - * 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. - * - * @return a new {@link Decoder}. - */ - public static Decoder ofByte() { - return new ByteDecoder(); - } - - /** - * Creates a {@link Decoder} for the 16-bit {@code short} type. - * - *

Note: It is often useful to apply {@link #withByteOrder(ByteOrder)} to the - * result of this method. - * - * @return a new {@link Decoder}. - */ - public static Decoder ofShort() { - return new ShortDecoder(); - } - - /** - * Creates a {@link Decoder} for the 32-bit {@code int} type. - * - *

Note: It is often useful to apply {@link #withByteOrder(ByteOrder)} to the - * result of this method. - * - * @return a new {@link Decoder}. - */ - public static Decoder ofInt() { - return new IntegerDecoder(); - } - - /** - * Creates a {@link Decoder} for the 64-bit {@code long} type. - * - *

Note: It is often useful to apply {@link #withByteOrder(ByteOrder)} to the - * result of this method. - * - * @return a new {@link Decoder}. - */ - public static Decoder ofLong() { - return new LongDecoder(); - } - - /** - * Creates a {@link Decoder} for the {@code double} type. - * - * @return a new {@link Decoder}. - */ - public static Decoder ofDouble() { - return new DoubleDecoder(); - } - - /** - * Creates a {@link Decoder} for the {@link String} type. - * - *

Note: 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 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 extends Decoder> { - - private final Decoder elementDecoder; - - ArrayDecoder(Decoder elementDecoder) { - this.elementDecoder = elementDecoder; - } - - @Override - public byte alignment() { - return elementDecoder.alignment(); - } - - @Override - @Nullable - Integer fixedSize() { - return null; - } - - @Override - public List decode(ByteBuffer byteSlice) { - List 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 extends Decoder> { - - private final Decoder elementDecoder; - - MaybeDecoder(Decoder elementDecoder) { - this.elementDecoder = elementDecoder; - } - - @Override - public byte alignment() { - return elementDecoder.alignment(); - } - - @Override - @Nullable - Integer fixedSize() { - return null; - } - - @Override - public Optional 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 extends Decoder { - - private final Class recordType; - 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.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; - } - - @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 { - - @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 { - - @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 { - - @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 { - - @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 { - - @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 { - - @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 { - - @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 { - - 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/src/main/java/eu/mulk/jgvariant/core/Signature.java b/src/main/java/eu/mulk/jgvariant/core/Signature.java deleted file mode 100644 index d9de5f1..0000000 --- a/src/main/java/eu/mulk/jgvariant/core/Signature.java +++ /dev/null @@ -1,116 +0,0 @@ -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. - * - *

Describes a type in the GVariant type system. The type can be arbitrarily complex. - * - *

Examples - * - *

- *
{@code "i"} - *
a single 32-bit integer - *
{@code "ai"} - *
an array of 32-bit integers - *
{@code "(bbb(sai))"} - *
a record consisting of three booleans and a nested record, which consists of a string and - * an array of 32-bit integers - *
- */ -@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 decoder() { - return (Decoder) 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> parseTupleTypes(ByteBuffer signature) throws ParseException { - 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 d1c1049..0000000 --- a/src/main/java/eu/mulk/jgvariant/core/Variant.java +++ /dev/null @@ -1,30 +0,0 @@ -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. - * - *

{@link #value()} can be of one of the following types: - * - *

    - *
  • {@link Boolean} - *
  • {@link Byte} - *
  • {@link Short} - *
  • {@link Integer} - *
  • {@link Long} - *
  • {@link String} - *
  • {@link java.util.Optional} (a GVariant {@code Maybe} type) - *
  • {@link java.util.List} (a GVariant array) - *
  • {@code Object[]} (a GVariant structure) - *
  • {@link Variant} (a nested variant) - *
- * - * @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/src/main/java/eu/mulk/jgvariant/core/package-info.java b/src/main/java/eu/mulk/jgvariant/core/package-info.java deleted file mode 100644 index 1754096..0000000 --- a/src/main/java/eu/mulk/jgvariant/core/package-info.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Provides {@link eu.mulk.jgvariant.core.Decoder}, the foundational class for GVariant parsing. - * - *

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

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

{@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 example = decoder.decode(ByteBuffer.wrap(bytes));
- * }
- */ -package eu.mulk.jgvariant.core; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java deleted file mode 100644 index 0282ff8..0000000 --- a/src/main/java/module-info.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Provides a parser for the GVariant - * serialization format. - * - * - * - *

Overview

- * - *

The {@link eu.mulk.jgvariant.core} package contains the {@link eu.mulk.jgvariant.core.Decoder} - * type, which contains classes to parse and represent serialized GVariant values. - * - *

Installation

- * - * - * - *

Usage with Maven

- * - *
{@code
- * 
- *   ...
- *
- *   
- *     ...
- *
- *     
- *       eu.mulk.jgvariant
- *       jgvariant-core
- *       0.1.3
- *     
- *
- *     ...
- *   
- *
- *   ...
- * 
- * }
- * - *

Usage with Gradle

- * - *
{@code
- * dependencies {
- *   ...
- *
- *   implementation("eu.mulk.jgvariant:jgvariant-core:0.1.3")
- *
- *   ...
- * }
- * }
- */ -module eu.mulk.jgvariant.core { - requires com.google.errorprone.annotations; - requires org.jetbrains.annotations; - requires org.apiguardian.api; - - exports eu.mulk.jgvariant.core; -} diff --git a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java deleted file mode 100644 index 5cf1a1c..0000000 --- a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java +++ /dev/null @@ -1,437 +0,0 @@ -package eu.mulk.jgvariant.core; - -import static java.nio.ByteOrder.BIG_ENDIAN; -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 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; - -/** - * Tests based on the examples given in ~desrt/gvariant-serialisation.pdf. - */ -class DecoderTest { - - @Test - void testString() { - var data = new byte[] {0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00}; - var decoder = Decoder.ofString(UTF_8); - assertEquals("hello world", decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testMaybe() { - var data = - new byte[] {0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00, 0x00}; - var decoder = Decoder.ofMaybe(Decoder.ofString(UTF_8)); - assertEquals(Optional.of("hello world"), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testBooleanArray() { - var data = new byte[] {0x01, 0x00, 0x00, 0x01, 0x01}; - var decoder = Decoder.ofArray(Decoder.ofBoolean()); - assertEquals(List.of(true, false, false, true, true), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testStructure() { - var data = - new byte[] { - 0x66, 0x6F, 0x6F, 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0x04 - }; - - record TestRecord(String s, int i) {} - - var decoder = Decoder.ofStructure(TestRecord.class, Decoder.ofString(UTF_8), Decoder.ofInt()); - assertEquals(new TestRecord("foo", -1), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testComplexStructureArray() { - var data = - new byte[] { - 0x68, - 0x69, - 0x00, - 0x00, - (byte) 0xfe, - (byte) 0xff, - (byte) 0xff, - (byte) 0xff, - 0x03, - 0x00, - 0x00, - 0x00, - 0x62, - 0x79, - 0x65, - 0x00, - (byte) 0xff, - (byte) 0xff, - (byte) 0xff, - (byte) 0xff, - 0x04, - 0x09, - 0x15 - }; - - record TestRecord(String s, int i) {} - - var decoder = - Decoder.ofArray( - Decoder.ofStructure( - TestRecord.class, - Decoder.ofString(UTF_8), - Decoder.ofInt().withByteOrder(LITTLE_ENDIAN))); - assertEquals( - List.of(new TestRecord("hi", -2), new TestRecord("bye", -1)), - decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testStringArray() { - var data = - new byte[] { - 0x69, 0x00, 0x63, 0x61, 0x6E, 0x00, 0x68, 0x61, 0x73, 0x00, 0x73, 0x74, 0x72, 0x69, 0x6E, - 0x67, 0x73, 0x3F, 0x00, 0x02, 0x06, 0x0a, 0x13 - }; - var decoder = Decoder.ofArray(Decoder.ofString(UTF_8)); - assertEquals(List.of("i", "can", "has", "strings?"), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testNestedStructure() { - 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 - }; - - record TestChild(byte b, String s) {} - record TestParent(TestChild tc, List as) {} - - var decoder = - Decoder.ofStructure( - TestParent.class, - Decoder.ofStructure(TestChild.class, Decoder.ofByte(), Decoder.ofString(UTF_8)), - Decoder.ofArray(Decoder.ofString(UTF_8))); - - assertEquals( - new TestParent(new TestChild((byte) 0x69, "can"), List.of("has", "strings?")), - 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 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])); - } - - @Test - void testSimpleStructure() { - var data = new byte[] {0x60, 0x70}; - - record TestRecord(byte b1, byte b2) {} - - var decoder = - Decoder.ofStructure( - TestRecord.class, - Decoder.ofByte().withByteOrder(LITTLE_ENDIAN), - Decoder.ofByte().withByteOrder(LITTLE_ENDIAN)); - - assertEquals(new TestRecord((byte) 0x60, (byte) 0x70), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testPaddedStructureRight() { - var data = new byte[] {0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00}; - - record TestRecord(int b1, byte b2) {} - - var decoder = - Decoder.ofStructure( - TestRecord.class, - Decoder.ofInt().withByteOrder(LITTLE_ENDIAN), - Decoder.ofByte().withByteOrder(LITTLE_ENDIAN)); - - assertEquals(new TestRecord(0x60, (byte) 0x70), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testPaddedStructureLeft() { - var data = new byte[] {0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00}; - - record TestRecord(byte b1, int b2) {} - - var decoder = - Decoder.ofStructure( - TestRecord.class, - Decoder.ofByte().withByteOrder(LITTLE_ENDIAN), - Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)); - - assertEquals(new TestRecord((byte) 0x60, 0x70), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testSimpleStructureArray() { - var data = - new byte[] { - 0x60, - 0x00, - 0x00, - 0x00, - 0x70, - 0x00, - 0x00, - 0x00, - (byte) 0x88, - 0x02, - 0x00, - 0x00, - (byte) 0xF7, - 0x00, - 0x00, - 0x00 - }; - - record TestRecord(int b1, byte b2) {} - - var decoder = - Decoder.ofArray( - Decoder.ofStructure( - TestRecord.class, - Decoder.ofInt().withByteOrder(LITTLE_ENDIAN), - Decoder.ofByte().withByteOrder(LITTLE_ENDIAN))); - - assertEquals( - List.of(new TestRecord(96, (byte) 0x70), new TestRecord(648, (byte) 0xf7)), - decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testByteArray() { - var data = new byte[] {0x04, 0x05, 0x06, 0x07}; - - var decoder = Decoder.ofArray(Decoder.ofByte()); - - assertEquals( - List.of((byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07), - decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testIntegerArray() { - var data = new byte[] {0x04, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00}; - - var decoder = Decoder.ofArray(Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)); - - assertEquals(List.of(4, 258), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testDictionaryEntry() { - var data = - new byte[] {0x61, 0x20, 0x6B, 0x65, 0x79, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x06}; - - record TestEntry(String key, int value) {} - - var decoder = - Decoder.ofStructure( - TestEntry.class, Decoder.ofString(UTF_8), Decoder.ofInt().withByteOrder(LITTLE_ENDIAN)); - assertEquals(new TestEntry("a key", 514), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testPaddedPrimitives() { - var data = - new byte[] { - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - record TestRecord(short s, long l, double d) {} - - var decoder = - Decoder.ofStructure( - TestRecord.class, - Decoder.ofShort().withByteOrder(BIG_ENDIAN), - Decoder.ofLong().withByteOrder(LITTLE_ENDIAN), - Decoder.ofDouble()); - assertEquals(new TestRecord((short) 1, 2, 3.25), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testEmbeddedMaybe() { - var data = new byte[] {0x01, 0x01}; - - record TestRecord(Optional set, Optional unset) {} - - var decoder = - Decoder.ofStructure( - TestRecord.class, Decoder.ofMaybe(Decoder.ofByte()), Decoder.ofMaybe(Decoder.ofByte())); - assertEquals( - new TestRecord(Optional.of((byte) 1), Optional.empty()), - decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testRecordComponentMismatch() { - record TestRecord(Optional set) {} - - var maybeDecoder = Decoder.ofMaybe(Decoder.ofByte()); - assertThrows( - IllegalArgumentException.class, - () -> Decoder.ofStructure(TestRecord.class, maybeDecoder, maybeDecoder)); - } - - @Test - void testTrivialRecord() { - var data = new byte[] {0x00}; - - record TestRecord() {} - - var decoder = Decoder.ofStructure(TestRecord.class); - assertEquals(new TestRecord(), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testTwoElementTrivialRecordArray() { - var data = new byte[] {0x00, 0x00}; - - record TestRecord() {} - - var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); - assertEquals( - List.of(new TestRecord(), new TestRecord()), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testSingletonTrivialRecordArray() { - var data = new byte[] {0x00}; - - record TestRecord() {} - - var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); - assertEquals(List.of(new TestRecord()), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testEmptyTrivialRecordArray() { - var data = new byte[] {}; - - record TestRecord() {} - - var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); - assertEquals(List.of(), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testVariantArray() { - var data = new byte[] {}; - - record TestRecord() {} - - var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class)); - assertEquals(List.of(), decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testInvalidVariantSignature() { - var data = new byte[] {0x00, 0x00, 0x2E}; - - var decoder = Decoder.ofVariant(); - assertThrows(IllegalArgumentException.class, () -> decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testMissingVariantSignature() { - var data = new byte[] {0x01}; - - var decoder = Decoder.ofVariant(); - assertThrows(IllegalArgumentException.class, () -> decoder.decode(ByteBuffer.wrap(data))); - } - - @Test - void testSimpleVariantRecord() throws ParseException { - // signature: "(bynqiuxtdsogvmiai)" - var data = - new byte[] { - 0x01, // b - 0x02, // y - 0x00, 0x03, // n - 0x00, 0x04, // q - 0x00, 0x00, // (padding) - 0x00, 0x00, 0x00, 0x05, // i - 0x00, 0x00, 0x00, 0x06, // u - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, // x - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // t - 0x40, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // d - 0x68, 0x69, 0x00, // s - 0x68, 0x69, 0x00, // o - 0x68, 0x69, 0x00, // g - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // (padding) - 0x00, 0x00, 0x00, 0x09, 0x00, 0x69, // v - 0x00, 0x00, // (padding) - 0x00, 0x00, 0x00, 0x0a, // mi - 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0c, // ai - 68, 62, 49, 46, 43, // framing offsets - 0x00, 0x28, 0x62, 0x79, 0x6E, 0x71, 0x69, 0x75, 0x78, 0x74, 0x64, 0x73, 0x6F, 0x67, 0x76, - 0x6D, 0x69, 0x61, 0x69, 0x29 - }; - - var decoder = Decoder.ofVariant(); - assertArrayEquals( - new Object[] { - true, - (byte) 2, - (short) 3, - (short) 4, - (int) 5, - (int) 6, - (long) 7, - (long) 8, - (double) 3.25, - "hi", - "hi", - "hi", - new Variant(Signature.parse("i"), 9), - Optional.of(10), - List.of(11, 12) - }, - (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()); - } -} -- cgit v1.2.3