diff options
Diffstat (limited to 'src/main/java/eu')
| -rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Decoder.java | 418 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/jgvariant/core/Value.java | 35 | 
2 files changed, 453 insertions, 0 deletions
diff --git a/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/src/main/java/eu/mulk/jgvariant/core/Decoder.java new file mode 100644 index 0000000..2ebd0af --- /dev/null +++ b/src/main/java/eu/mulk/jgvariant/core/Decoder.java @@ -0,0 +1,418 @@ +package eu.mulk.jgvariant.core; + +import static java.nio.ByteOrder.LITTLE_ENDIAN; + +import eu.mulk.jgvariant.core.Value.Array; +import eu.mulk.jgvariant.core.Value.Bool; +import eu.mulk.jgvariant.core.Value.Float64; +import eu.mulk.jgvariant.core.Value.Int16; +import eu.mulk.jgvariant.core.Value.Int32; +import eu.mulk.jgvariant.core.Value.Int64; +import eu.mulk.jgvariant.core.Value.Int8; +import eu.mulk.jgvariant.core.Value.Maybe; +import eu.mulk.jgvariant.core.Value.Str; +import eu.mulk.jgvariant.core.Value.Structure; +import eu.mulk.jgvariant.core.Value.Variant; +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.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.Nullable; + +/** + * Type class for decodable {@link Value} types. + * + * <p>Use the {@code of*} family of constructor methods to acquire a suitable {@link Decoder} for + * the type you wish to decode. + * + * @param <T> the type that the {@link Decoder} can decode. + */ +@SuppressWarnings("java:S1610") +public abstract class Decoder<T extends Value> { + +  private Decoder() {} + +  /** +   * @throws java.nio.BufferUnderflowException if the byte buffer is shorter than the requested +   *     data. +   */ +  public abstract T decode(ByteBuffer byteSlice); + +  abstract byte alignment(); + +  @Nullable +  abstract Integer fixedSize(); + +  final boolean hasFixedSize() { +    return fixedSize() != null; +  } + +  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); +      } +    }; +  } + +  public static <U extends Value> Decoder<Array<U>> ofArray(Decoder<U> elementDecoder) { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return elementDecoder.alignment(); +      } + +      @Override +      @Nullable +      Integer fixedSize() { +        return null; +      } + +      @Override +      public Array<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 new Array<>(elements); +      } +    }; +  } + +  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; +  } + +  public static <U extends Value> Decoder<Maybe<U>> ofMaybe(Decoder<U> elementDecoder) { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return elementDecoder.alignment(); +      } + +      @Override +      @Nullable +      Integer fixedSize() { +        return null; +      } + +      @Override +      public Maybe<U> decode(ByteBuffer byteSlice) { +        if (!byteSlice.hasRemaining()) { +          return new Maybe<>(Optional.empty()); +        } else { +          if (!elementDecoder.hasFixedSize()) { +            // Remove trailing zero byte. +            byteSlice.limit(byteSlice.limit() - 1); +          } + +          return new Maybe<>(Optional.of(elementDecoder.decode(byteSlice))); +        } +      } +    }; +  } + +  @SafeVarargs +  public static <U extends Record> Decoder<Structure<U>> ofStructure( +      Class<U> recordType, Decoder<? extends Value>... 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)); +    } + +    return new Decoder<>() { +      @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 Structure<U> decode(ByteBuffer byteSlice) { +        int framingOffsetSize = byteCount(byteSlice.limit()); + +        var recordConstructorArguments = new Object[recordComponents.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) { +            recordConstructorArguments[componentIndex] = +                componentDecoder.decode(byteSlice.slice(position, fixedComponentSize)); +            position += fixedComponentSize; +          } else { +            if (componentIndex == recordComponents.length - 1) { +              // The last component never has a framing offset. +              int endPosition = byteSlice.limit() - framingOffsetIndex * framingOffsetSize; +              recordConstructorArguments[componentIndex] = +                  componentDecoder.decode(byteSlice.slice(position, endPosition - position)); +              position = endPosition; +            } else { +              int framingOffset = +                  getIntN( +                      byteSlice.slice( +                          byteSlice.limit() - (1 + framingOffsetIndex) * framingOffsetSize, +                          framingOffsetSize)); +              recordConstructorArguments[componentIndex] = +                  componentDecoder.decode(byteSlice.slice(position, framingOffset - position)); +              position = framingOffset; +              ++framingOffsetIndex; +            } +          } + +          ++componentIndex; +        } + +        try { +          var recordComponentTypes = +              Arrays.stream(recordType.getRecordComponents()) +                  .map(RecordComponent::getType) +                  .toArray(Class<?>[]::new); +          var recordConstructor = recordType.getDeclaredConstructor(recordComponentTypes); +          return new Structure<>(recordConstructor.newInstance(recordConstructorArguments)); +        } catch (NoSuchMethodException +            | InstantiationException +            | IllegalAccessException +            | InvocationTargetException e) { +          throw new IllegalStateException(e); +        } +      } +    }; +  } + +  public static Decoder<Variant> ofVariant() { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 8; +      } + +      @Override +      @Nullable +      Integer fixedSize() { +        return null; +      } + +      @Override +      public Variant decode(ByteBuffer byteSlice) { +        // TODO +        throw new UnsupportedOperationException("not implemented"); +      } +    }; +  } + +  public static Decoder<Bool> ofBoolean() { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 1; +      } + +      @Override +      public Integer fixedSize() { +        return 1; +      } + +      @Override +      public Bool decode(ByteBuffer byteSlice) { +        return new Bool(byteSlice.get() != 0); +      } +    }; +  } + +  public static Decoder<Int8> ofInt8() { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 1; +      } + +      @Override +      public Integer fixedSize() { +        return 1; +      } + +      @Override +      public Int8 decode(ByteBuffer byteSlice) { +        return new Int8(byteSlice.get()); +      } +    }; +  } + +  public static Decoder<Int16> ofInt16() { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 2; +      } + +      @Override +      public Integer fixedSize() { +        return 2; +      } + +      @Override +      public Int16 decode(ByteBuffer byteSlice) { +        return new Int16(byteSlice.getShort()); +      } +    }; +  } + +  public static Decoder<Int32> ofInt32() { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 4; +      } + +      @Override +      public Integer fixedSize() { +        return 4; +      } + +      @Override +      public Int32 decode(ByteBuffer byteSlice) { +        return new Int32(byteSlice.getInt()); +      } +    }; +  } + +  public static Decoder<Int64> ofInt64() { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 8; +      } + +      @Override +      public Integer fixedSize() { +        return 8; +      } + +      @Override +      public Int64 decode(ByteBuffer byteSlice) { +        return new Int64(byteSlice.getLong()); +      } +    }; +  } + +  public static Decoder<Float64> ofFloat64() { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 8; +      } + +      @Override +      public Integer fixedSize() { +        return 8; +      } + +      @Override +      public Float64 decode(ByteBuffer byteSlice) { +        return new Float64(byteSlice.getDouble()); +      } +    }; +  } + +  public static Decoder<Str> ofStr(Charset charset) { +    return new Decoder<>() { +      @Override +      public byte alignment() { +        return 1; +      } + +      @Override +      @Nullable +      Integer fixedSize() { +        return null; +      } + +      @Override +      public Str decode(ByteBuffer byteSlice) { +        byteSlice.limit(byteSlice.limit() - 1); +        return new Str(charset.decode(byteSlice).toString()); +      } +    }; +  } +} diff --git a/src/main/java/eu/mulk/jgvariant/core/Value.java b/src/main/java/eu/mulk/jgvariant/core/Value.java new file mode 100644 index 0000000..969f240 --- /dev/null +++ b/src/main/java/eu/mulk/jgvariant/core/Value.java @@ -0,0 +1,35 @@ +package eu.mulk.jgvariant.core; + +import java.util.List; +import java.util.Optional; + +/** A value representable by the GVariant serialization format. */ +public sealed interface Value { + +  // Composite types +  record Array<T extends Value>(List<T> values) implements Value {} + +  record Maybe<T extends Value>(Optional<T> value) implements Value {} + +  record Structure<T extends Record>(T values) implements Value {} + +  record Variant(Class<? extends Value> type, Value value) implements Value {} + +  // Primitive types +  record Bool(boolean value) implements Value { +    static Bool TRUE = new Bool(true); +    static Bool FALSE = new Bool(false); +  } + +  record Int8(byte value) implements Value {} + +  record Int16(short value) implements Value {} + +  record Int32(int value) implements Value {} + +  record Int64(long value) implements Value {} + +  record Float64(double value) implements Value {} + +  record Str(String value) implements Value {} +}  | 
