Add Decoder#encode roundtrip tests and fix the bugs discovered.
Change-Id: I21447306d9fc7768e07fafe5bed1d92a3eb42e53
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
index a28d792..f605b09 100644
--- a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java
+++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java
@@ -5,6 +5,7 @@
package eu.mulk.jgvariant.core;
import static java.lang.Math.max;
+import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNullElse;
@@ -440,12 +441,16 @@
ArrayList<Integer> framingOffsets = new ArrayList<>(value.size());
int startOffset = byteWriter.position();
for (var element : value) {
+ // Align the element.
+ var lastRelativeEnd = byteWriter.position() - startOffset;
+ byteWriter.write(new byte[align(lastRelativeEnd, alignment()) - lastRelativeEnd]);
+
+ // Encode the element.
elementDecoder.encode(element, byteWriter);
+
+ // Record the framing offset of the element.
var relativeEnd = byteWriter.position() - startOffset;
framingOffsets.add(relativeEnd);
-
- // Align the next element.
- byteWriter.write(new byte[align(relativeEnd, alignment()) - relativeEnd]);
}
// Write the framing offsets.
@@ -710,21 +715,30 @@
@Override
@SuppressWarnings("unchecked")
void encode(Object[] value, ByteWriter byteWriter) {
+ // The unit type is encoded as a single zero byte.
+ if (value.length == 0) {
+ byteWriter.write((byte) 0);
+ return;
+ }
+
int startOffset = byteWriter.position();
ArrayList<Integer> framingOffsets = new ArrayList<>(value.length);
for (int i = 0; i < value.length; ++i) {
var componentDecoder = (Decoder<Object>) componentDecoders[i];
+
+ // Align the element.
+ var lastRelativeEnd = byteWriter.position() - startOffset;
+ byteWriter.write(new byte[align(lastRelativeEnd, componentDecoder.alignment()) - lastRelativeEnd]);
+
+ // Encode the element.
componentDecoder.encode(value[i], byteWriter);
- var relativeEnd = byteWriter.position() - startOffset;
-
+ // Record the framing offset of the element if it is of variable size.
var fixedComponentSize = componentDecoders[i].fixedSize();
if (fixedComponentSize == null && i < value.length - 1) {
+ var relativeEnd = byteWriter.position() - startOffset;
framingOffsets.add(relativeEnd);
}
-
- // Align the next element.
- byteWriter.write(new byte[align(relativeEnd, alignment()) - relativeEnd]);
}
// Write the framing offsets in reverse order.
@@ -732,6 +746,12 @@
for (int i = framingOffsets.size() - 1; i >= 0; --i) {
byteWriter.writeIntN(framingOffsets.get(i), framingOffsetSize);
}
+
+ // Pad the structure to its alignment if it is of fixed size.
+ if (fixedSize() != null) {
+ var lastRelativeEnd = byteWriter.position() - startOffset;
+ byteWriter.write(new byte[align(lastRelativeEnd, alignment()) - lastRelativeEnd]);
+ }
}
}
@@ -975,7 +995,7 @@
@Override
void encode(String value, ByteWriter byteWriter) {
- byteWriter.write(charset.encode(value));
+ byteWriter.write(charset.encode(value).rewind());
byteWriter.write((byte) 0);
}
}
@@ -1044,7 +1064,7 @@
var innerByteWriter = new ByteWriter();
Decoder.this.encode(value, innerByteWriter);
var transformedBuffer = encodingFunction.apply(innerByteWriter.toByteBuffer());
- byteWriter.write(transformedBuffer);
+ byteWriter.write(transformedBuffer.rewind());
}
}
@@ -1136,7 +1156,7 @@
}
private static class ByteWriter {
- private ByteOrder byteOrder = ByteOrder.nativeOrder();
+ private ByteOrder byteOrder = BIG_ENDIAN;
private final ByteArrayOutputStream outputStream;
ByteWriter() {
@@ -1167,19 +1187,19 @@
}
void write(int value) {
- write(ByteBuffer.allocate(4).order(byteOrder).putInt(value));
+ write(ByteBuffer.allocate(4).order(byteOrder).putInt(value).rewind());
}
void write(long value) {
- write(ByteBuffer.allocate(8).order(byteOrder).putLong(value));
+ write(ByteBuffer.allocate(8).order(byteOrder).putLong(value).rewind());
}
void write(short value) {
- write(ByteBuffer.allocate(2).order(byteOrder).putShort(value));
+ write(ByteBuffer.allocate(2).order(byteOrder).putShort(value).rewind());
}
void write(double value) {
- write(ByteBuffer.allocate(8).order(byteOrder).putDouble(value));
+ write(ByteBuffer.allocate(8).order(byteOrder).putDouble(value).rewind());
}
private void writeIntN(int value, int byteCount) {
@@ -1195,7 +1215,7 @@
default ->
throw new IllegalArgumentException("invalid byte count: %d".formatted(byteCount));
}
- write(byteBuffer);
+ write(byteBuffer.rewind());
}
ByteWriter duplicate() {
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
index 068b051..d97cf88 100644
--- a/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
+++ b/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
@@ -14,9 +14,7 @@
import java.nio.ByteBuffer;
import java.text.ParseException;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import org.junit.jupiter.api.Test;
/**
@@ -24,13 +22,14 @@
* href="https://people.gnome.org/~desrt/gvariant-serialisation.pdf">~desrt/gvariant-serialisation.pdf</a>.
*/
@SuppressWarnings({
+ "ByteBufferBackingArray",
"ImmutableListOf",
"ImmutableListOf1",
"ImmutableListOf2",
"ImmutableListOf3",
"ImmutableListOf4",
"ImmutableListOf5",
- "ImmutableMapOf2"
+ "ImmutableMapOf2",
})
class DecoderTest {
@@ -39,6 +38,9 @@
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)));
+
+ var roundtripData = decoder.encode("hello world");
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -47,6 +49,9 @@
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)));
+
+ var roundtripData = decoder.encode(Optional.of("hello world"));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -54,6 +59,9 @@
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)));
+
+ var roundtripData = decoder.encode(List.of(true, false, false, true, true));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -67,6 +75,9 @@
var decoder = Decoder.ofStructure(TestRecord.class, Decoder.ofString(UTF_8), Decoder.ofInt());
assertEquals(new TestRecord("foo", -1), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestRecord("foo", -1));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -109,6 +120,10 @@
assertEquals(
List.of(new TestRecord("hi", -2), new TestRecord("bye", -1)),
decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData =
+ decoder.encode(List.of(new TestRecord("hi", -2), new TestRecord("bye", -1)));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -143,6 +158,13 @@
var decoder =
Decoder.ofDictionary(Decoder.ofString(UTF_8), Decoder.ofInt().withByteOrder(LITTLE_ENDIAN));
assertEquals(Map.of("hi", -2, "bye", -1), decoder.decode(ByteBuffer.wrap(data)));
+
+ var entity = new LinkedHashMap<String, Integer>();
+ entity.put("hi", -2);
+ entity.put("bye", -1);
+ var roundtripData = decoder.encode(entity);
+ System.out.println(HexFormat.of().formatHex(roundtripData.array()));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -154,6 +176,9 @@
};
var decoder = Decoder.ofArray(Decoder.ofString(UTF_8));
assertEquals(List.of("i", "can", "has", "strings?"), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(List.of("i", "can", "has", "strings?"));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -176,6 +201,11 @@
assertEquals(
new TestParent(new TestChild((byte) 0x69, "can"), List.of("has", "strings?")),
decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData =
+ decoder.encode(
+ new TestParent(new TestChild((byte) 0x69, "can"), List.of("has", "strings?")));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -195,6 +225,9 @@
() -> assertEquals(2, result.length),
() -> assertArrayEquals(new Object[] {(byte) 0x69, "can"}, (Object[]) result[0]),
() -> assertEquals(List.of("has", "strings?"), result[1]));
+
+ var roundtripData = decoder.encode(variant);
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -210,6 +243,9 @@
Decoder.ofByte().withByteOrder(LITTLE_ENDIAN));
assertEquals(new TestRecord((byte) 0x60, (byte) 0x70), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestRecord((byte) 0x60, (byte) 0x70));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -225,6 +261,9 @@
Decoder.ofByte().withByteOrder(LITTLE_ENDIAN));
assertEquals(new TestRecord(0x60, (byte) 0x70), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestRecord(0x60, (byte) 0x70));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -240,6 +279,9 @@
Decoder.ofInt().withByteOrder(LITTLE_ENDIAN));
assertEquals(new TestRecord((byte) 0x60, 0x70), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestRecord((byte) 0x60, 0x70));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -276,6 +318,10 @@
assertEquals(
List.of(new TestRecord(96, (byte) 0x70), new TestRecord(648, (byte) 0xf7)),
decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData =
+ decoder.encode(List.of(new TestRecord(96, (byte) 0x70), new TestRecord(648, (byte) 0xf7)));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -287,6 +333,9 @@
assertEquals(
List.of((byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07),
decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(List.of((byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -296,6 +345,9 @@
var decoder = Decoder.ofByteArray();
assertArrayEquals(data, decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(data);
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -307,6 +359,9 @@
var decoder = Decoder.ofStructure(TestRecord.class, Decoder.ofByteArray());
assertArrayEquals(data, decoder.decode(ByteBuffer.wrap(data)).bytes());
+
+ var roundtripData = decoder.encode(new TestRecord(data));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -316,6 +371,9 @@
var decoder = Decoder.ofArray(Decoder.ofInt().withByteOrder(LITTLE_ENDIAN));
assertEquals(List.of(4, 258), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(List.of(4, 258));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -327,6 +385,9 @@
Decoder.ofDictionaryEntry(
Decoder.ofString(UTF_8), Decoder.ofInt().withByteOrder(LITTLE_ENDIAN));
assertEquals(Map.entry("a key", 514), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(Map.entry("a key", 514));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -340,6 +401,9 @@
Decoder.ofStructure(
TestEntry.class, Decoder.ofString(UTF_8), Decoder.ofInt().withByteOrder(LITTLE_ENDIAN));
assertEquals(new TestEntry("a key", 514), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestEntry("a key", 514));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -359,6 +423,10 @@
Decoder.ofLong().withByteOrder(LITTLE_ENDIAN),
Decoder.ofDouble());
assertEquals(new TestRecord((short) 1, 2, 3.25), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestRecord((short) 1, 2, 3.25));
+
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -373,6 +441,9 @@
assertEquals(
new TestRecord(Optional.of((byte) 1), Optional.empty()),
decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestRecord(Optional.of((byte) 1), Optional.empty()));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -393,6 +464,9 @@
var decoder = Decoder.ofStructure(TestRecord.class);
assertEquals(new TestRecord(), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(new TestRecord());
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -404,6 +478,9 @@
var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class));
assertEquals(
List.of(new TestRecord(), new TestRecord()), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(List.of(new TestRecord(), new TestRecord()));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -414,6 +491,9 @@
var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class));
assertEquals(List.of(new TestRecord()), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(List.of(new TestRecord()));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -424,6 +504,9 @@
var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class));
assertEquals(List.of(), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(List.of());
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -434,6 +517,9 @@
var decoder = Decoder.ofArray(Decoder.ofStructure(TestRecord.class));
assertEquals(List.of(), decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData = decoder.encode(List.of());
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -500,6 +586,9 @@
List.of(11, 12)
},
(Object[]) decoder.decode(ByteBuffer.wrap(data)).value());
+
+ var roundtripData = decoder.encode(decoder.decode(ByteBuffer.wrap(data)));
+ assertArrayEquals(data, roundtripData.array());
}
@Test
@@ -610,5 +699,14 @@
new TestChild((short) 5, (short) 6),
new TestChild((short) 7, (short) 8)),
decoder.decode(ByteBuffer.wrap(data)));
+
+ var roundtripData =
+ decoder.encode(
+ new TestParent(
+ new TestChild((short) 1, (short) 2),
+ new TestChild((short) 3, (short) 4),
+ new TestChild((short) 5, (short) 6),
+ new TestChild((short) 7, (short) 8)));
+ assertArrayEquals(data, roundtripData.array());
}
}