diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2021-12-18 23:38:46 +0100 | 
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2021-12-18 23:38:46 +0100 | 
| commit | 796b19da1b9ef6c1721faa2ddf35100eb01a8a28 (patch) | |
| tree | 44c4edf40e73b11d09535d011de95be71d0fdec0 /jgvariant-core/src | |
| parent | 8bbc9c1a04177dc0bc0082cc59d98d57eead4c1f (diff) | |
POM: Split into -bom, -core, -parent, -bundle.
Change-Id: I1fd4cc766b60266ef9dcc40e943b45d067dd7b90
Diffstat (limited to 'jgvariant-core/src')
6 files changed, 1341 insertions, 0 deletions
| 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. + * + * <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(String s, int i) {} + * + * var decoder = + *   Decoder.ofArray( + *     Decoder.ofStructure( + *       ExampleRecord.class, + *       Decoder.ofString(UTF_8), + *       Decoder.ofInt().withByteOrder(LITTLE_ENDIAN))); + * + * byte[] bytes = ...; + * List<ExampleRecord> example = decoder.decode(ByteBuffer.wrap(bytes)); + * }</pre> + * + * @param <T> the type that the {@link Decoder} can decode. + */ +@SuppressWarnings("java:S1610") +@API(status = Status.EXPERIMENTAL) +public abstract class Decoder<T> { + +  private Decoder() {} + +  /** +   * Decodes a {@link ByteBuffer} holding a serialized GVariant into a value of type {@code T}. +   * +   * <p><strong>Note:</strong> 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<T> 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 <U> the element type. +   * @return a new {@link Decoder}. +   */ +  public static <U> Decoder<List<U>> ofArray(Decoder<U> elementDecoder) { +    return new ArrayDecoder<>(elementDecoder); +  } + +  /** +   * Creates a {@link Decoder} for a {@code Maybe} type. +   * +   * @param elementDecoder a {@link Decoder} for the contained element. +   * @param <U> the element type. +   * @return a new {@link Decoder}. +   */ +  public static <U> Decoder<Optional<U>> ofMaybe(Decoder<U> 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 <U> the {@link Record} type that represents the components of the structure. +   * @return a new {@link Decoder}. +   */ +  public static <U extends Record> Decoder<U> ofStructure( +      Class<U> recordType, Decoder<?>... componentDecoders) { +    return new StructureDecoder<>(recordType, componentDecoders); +  } + +  /** +   * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link List}. +   * +   * <p>Prefer {@link #ofStructure(Class, Decoder[])} if possible, which is both more type-safe and +   * more convenient. +   * +   * @param componentDecoders a {@link Decoder} for each component of the structure. +   * @return a new {@link Decoder}. +   */ +  public static Decoder<Object[]> ofStructure(Decoder<?>... componentDecoders) { +    return new TupleDecoder(componentDecoders); +  } + +  /** +   * Creates a {@link Decoder} for the {@link Variant} type. +   * +   * <p>The contained {@link Object} can be of one of the following types: +   * +   * <ul> +   *   <li>{@link Boolean} +   *   <li>{@link Byte} +   *   <li>{@link Short} +   *   <li>{@link Integer} +   *   <li>{@link Long} +   *   <li>{@link String} +   *   <li>{@link Optional} (a GVariant {@code Maybe} type) +   *   <li>{@link List} (a GVariant array) +   *   <li>{@code Object[]} (a GVariant structure) +   *   <li>{@link Variant} (a nested variant) +   * </ul> +   * +   * @return a new {@link Decoder}. +   */ +  public static Decoder<Variant> ofVariant() { +    return new VariantDecoder(); +  } + +  /** +   * Creates a {@link Decoder} for the {@code boolean} type. +   * +   * @return a new {@link Decoder}. +   */ +  public static Decoder<Boolean> ofBoolean() { +    return new BooleanDecoder(); +  } + +  /** +   * Creates a {@link Decoder} for the 8-bit {@code byte} type. +   * +   * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the +   * result of this method. +   * +   * @return a new {@link Decoder}. +   */ +  public static Decoder<Byte> ofByte() { +    return new ByteDecoder(); +  } + +  /** +   * Creates a {@link Decoder} for the 16-bit {@code short} 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<Short> ofShort() { +    return new ShortDecoder(); +  } + +  /** +   * Creates a {@link Decoder} for the 32-bit {@code int} 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<Integer> ofInt() { +    return new IntegerDecoder(); +  } + +  /** +   * Creates a {@link Decoder} for the 64-bit {@code long} 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<Long> ofLong() { +    return new LongDecoder(); +  } + +  /** +   * Creates a {@link Decoder} for the {@code double} type. +   * +   * @return a new {@link Decoder}. +   */ +  public static Decoder<Double> ofDouble() { +    return new DoubleDecoder(); +  } + +  /** +   * Creates a {@link Decoder} for the {@link String} 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. +   * +   * @param charset the {@link Charset} the string is encoded in. +   * @return a new {@link Decoder}. +   */ +  public static Decoder<String> 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<U> extends Decoder<List<U>> { + +    private final Decoder<U> elementDecoder; + +    ArrayDecoder(Decoder<U> elementDecoder) { +      this.elementDecoder = elementDecoder; +    } + +    @Override +    public byte alignment() { +      return elementDecoder.alignment(); +    } + +    @Override +    @Nullable +    Integer fixedSize() { +      return null; +    } + +    @Override +    public List<U> decode(ByteBuffer byteSlice) { +      List<U> 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<U> extends Decoder<Optional<U>> { + +    private final Decoder<U> elementDecoder; + +    MaybeDecoder(Decoder<U> elementDecoder) { +      this.elementDecoder = elementDecoder; +    } + +    @Override +    public byte alignment() { +      return elementDecoder.alignment(); +    } + +    @Override +    @Nullable +    Integer fixedSize() { +      return null; +    } + +    @Override +    public Optional<U> 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<U extends Record> extends Decoder<U> { + +    private final Class<U> recordType; +    private final TupleDecoder tupleDecoder; + +    StructureDecoder(Class<U> recordType, Decoder<?>... componentDecoders) { +      var recordComponents = recordType.getRecordComponents(); +      if (componentDecoders.length != recordComponents.length) { +        throw new IllegalArgumentException( +            "number of decoders (%d) does not match number of structure components (%d)" +                .formatted(componentDecoders.length, recordComponents.length)); +      } + +      this.recordType = recordType; +      this.tupleDecoder = new TupleDecoder(componentDecoders); +    } + +    @Override +    public byte alignment() { +      return tupleDecoder.alignment(); +    } + +    @Override +    public Integer fixedSize() { +      return tupleDecoder.fixedSize(); +    } + +    @Override +    public U decode(ByteBuffer byteSlice) { +      Object[] recordConstructorArguments = tupleDecoder.decode(byteSlice); + +      try { +        var recordComponentTypes = +            Arrays.stream(recordType.getRecordComponents()) +                .map(RecordComponent::getType) +                .toArray(Class<?>[]::new); +        var recordConstructor = recordType.getDeclaredConstructor(recordComponentTypes); +        return recordConstructor.newInstance(recordConstructorArguments); +      } catch (NoSuchMethodException +          | InstantiationException +          | IllegalAccessException +          | InvocationTargetException e) { +        throw new IllegalStateException(e); +      } +    } +  } + +  private static class TupleDecoder extends Decoder<Object[]> { + +    private final Decoder<?>[] componentDecoders; + +    TupleDecoder(Decoder<?>... componentDecoders) { +      this.componentDecoders = componentDecoders; +    } + +    @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<Variant> { + +    @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<Boolean> { + +    @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<Byte> { + +    @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<Short> { + +    @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<Integer> { + +    @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<Long> { + +    @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<Double> { + +    @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<String> { + +    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. + * + * <p>Describes a type in the GVariant type system. The type can be arbitrarily complex. + * + * <p><strong>Examples</strong> + * + * <dl> + *   <dt>{@code "i"} + *   <dd>a single 32-bit integer + *   <dt>{@code "ai"} + *   <dd>an array of 32-bit integers + *   <dt>{@code "(bbb(sai))"} + *   <dd>a record consisting of three booleans and a nested record, which consists of a string and + *       an array of 32-bit integers + * </dl> + */ +@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<Object> decoder() { +    return (Decoder<Object>) decoder; +  } + +  /** +   * Returns the signature formatted as a GVariant signature string. +   * +   * @return a GVariant signature string. +   */ +  @Override +  public String toString() { +    return signatureString; +  } + +  @Override +  public boolean equals(Object o) { +    return (o instanceof Signature signature) +        && Objects.equals(signatureString, signature.signatureString); +  } + +  @Override +  public int hashCode() { +    return Objects.hash(signatureString); +  } + +  private static Decoder<?> parseSignature(ByteBuffer signature) throws ParseException { +    char c = (char) signature.get(); +    return switch (c) { +      case 'b' -> Decoder.ofBoolean(); +      case 'y' -> Decoder.ofByte(); +      case 'n', 'q' -> Decoder.ofShort(); +      case 'i', 'u' -> Decoder.ofInt(); +      case 'x', 't' -> Decoder.ofLong(); +      case 'd' -> Decoder.ofDouble(); +      case 's', 'o', 'g' -> Decoder.ofString(StandardCharsets.UTF_8); +      case 'v' -> Decoder.ofVariant(); +      case 'm' -> Decoder.ofMaybe(parseSignature(signature)); +      case 'a' -> Decoder.ofArray(parseSignature(signature)); +      case '(', '{' -> Decoder.ofStructure(parseTupleTypes(signature).toArray(new Decoder<?>[0])); +      default -> throw new ParseException( +          String.format("encountered unknown signature byte '%c'", c), signature.position()); +    }; +  } + +  private static List<Decoder<?>> parseTupleTypes(ByteBuffer signature) throws ParseException { +    List<Decoder<?>> decoders = new ArrayList<>(); + +    while (true) { +      char c = (char) signature.get(signature.position()); +      if (c == ')' || c == '}') { +        signature.get(); +        break; +      } + +      decoders.add(parseSignature(signature)); +    } + +    return decoders; +  } +} diff --git a/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. + * + * <p>{@link #value()} can be of one of the following types: + * + * <ul> + *   <li>{@link Boolean} + *   <li>{@link Byte} + *   <li>{@link Short} + *   <li>{@link Integer} + *   <li>{@link Long} + *   <li>{@link String} + *   <li>{@link java.util.Optional} (a GVariant {@code Maybe} type) + *   <li>{@link java.util.List} (a GVariant array) + *   <li>{@code Object[]} (a GVariant structure) + *   <li>{@link Variant} (a nested variant) + * </ul> + * + * @param signature the signature describing the type of the value. + * @param value the value itself; one of {@link Boolean}, {@link Byte}, {@link Short}, {@link + *     Integer}, {@link Long}, {@link String}, {@link java.util.Optional}, {@link java.util.List}, + *     {@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 <a + * href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> parsing. + * + * <p>Instances of {@link eu.mulk.jgvariant.core.Decoder} read a given value type from a {@link + * java.nio.ByteBuffer}. The class also contains factory methods to create those instances. + * + * <p><strong>Example</strong> + * + * <p>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: + * + * <pre>{@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<ExampleRecord> example = decoder.decode(ByteBuffer.wrap(bytes)); + * }</pre> + */ +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 <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> + * serialization format. + * + * <ul> + *   <li><a href="#sect-overview">Overview</a> + *   <li><a href="#sect-installation">Installation</a> + * </ul> + * + * <h2 id="sect-overview">Overview</h2> + * + * <p>The {@link eu.mulk.jgvariant.core} package contains the {@link eu.mulk.jgvariant.core.Decoder} + * type, which contains classes to parse and represent serialized <a + * href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> values. + * + * <h2 id="sect-installation">Installation</h2> + * + * <ul> + *   <li><a href="#sect-installation-maven">Usage with Maven</a> + *   <li><a href="#sect-installation-gradle">Usage with Gradle</a> + * </ul> + * + * <h3 id="sect-installation-maven">Usage with Maven</h3> + * + * <pre>{@code + * <project> + *   ... + * + *   <dependencyManagement> + *     ... + * + *     <dependencies> + *       <dependency> + *         <groupId>eu.mulk.jgvariant</groupId> + *         <artifactId>jgvariant-bom</artifactId> + *         <version>0.1.4</version> + *         <type>pom</type> + *         <scope>import</scope> + *       </dependency> + *     </dependencies> + * + *     ... + *   </dependencyManagement> + * + *   <dependencies> + *     ... + * + *     <dependency> + *       <groupId>eu.mulk.jgvariant</groupId> + *       <artifactId>jgvariant-core</artifactId> + *     </dependency> + * + *     ... + *   </dependencies> + * + *   ... + * </project> + * }</pre> + * + * <h3 id="sect-installation-gradle">Usage with Gradle</h3> + * + * <pre>{@code + * dependencies { + *   ... + * + *   implementation(platform("eu.mulk.jgvariant:jgvariant-bom:0.1.4") + *   implementation("eu.mulk.jgvariant:jgvariant-core") + * + *   ... + * } + * }</pre> + */ +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 <a + * href="https://people.gnome.org/~desrt/gvariant-serialisation.pdf">~desrt/gvariant-serialisation.pdf</a>. + */ +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<String> 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<Byte> set, Optional<Byte> 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<Byte> 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()); +  } +} | 
