Add OSTree encoding roundtrip tests and fix the bugs discovered.

Change-Id: I4c81329c5381d6ae843fee5da2bed035941011e3
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 d97cf88..d8fa6a5 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
@@ -163,7 +163,6 @@
     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());
   }
 
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Checksum.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Checksum.java
index 829664e..e9bdf84 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Checksum.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Checksum.java
@@ -18,7 +18,11 @@
   private static final int SIZE = 32;
 
   private static final Decoder<Checksum> DECODER =
-      ByteString.decoder().map(Checksum::new, Checksum::byteString);
+      ByteString.decoder().map(Checksum::new, Checksum::optimizedByteString);
+
+  private static final ByteString NULL_BYTESTRING = new ByteString(new byte[0]);
+
+  private static final Checksum ZERO = new Checksum(new ByteString(new byte[SIZE]));
 
   public Checksum {
     if (byteString.size() == 0) {
@@ -47,7 +51,11 @@
    * @return a checksum whose bits are all zero.
    */
   public static Checksum zero() {
-    return new Checksum(new ByteString(new byte[SIZE]));
+    return ZERO;
+  }
+
+  public ByteString optimizedByteString() {
+    return isEmpty() ? NULL_BYTESTRING : byteString;
   }
 
   /**
@@ -56,7 +64,7 @@
    * @return {@code true} if the byte string is equal to {@link #zero()}, {@code false} otherwise.
    */
   public boolean isEmpty() {
-    return equals(zero());
+    return equals(ZERO);
   }
 
   /**
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaMetaEntry.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaMetaEntry.java
index 2be0426..71419c6 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaMetaEntry.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaMetaEntry.java
@@ -67,8 +67,8 @@
     var output = new ByteArrayOutputStream();
 
     for (var deltaObject : deltaObjects) {
-      output.write(deltaObject.objectType.byteValue());
-      output.writeBytes(deltaObject.checksum.byteString().bytes());
+      output.write(deltaObject.objectType().byteValue());
+      output.writeBytes(deltaObject.checksum().byteString().bytes());
     }
 
     return output.toByteArray();
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperation.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperation.java
index d753a48..42b7056 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperation.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperation.java
@@ -69,16 +69,16 @@
   }
 
   default void writeTo(ByteArrayOutputStream output) {
-    if (this instanceof OpenSpliceAndCloseMeta openSpliceAndCloseMeta) {
-      output.write(DeltaOperationType.OPEN_SPLICE_AND_CLOSE.byteValue());
-      writeVarint64(output, openSpliceAndCloseMeta.offset);
-      writeVarint64(output, openSpliceAndCloseMeta.size);
-    } else if (this instanceof OpenSpliceAndCloseReal openSpliceAndCloseReal) {
+    if (this instanceof OpenSpliceAndCloseReal openSpliceAndCloseReal) {
       output.write(DeltaOperationType.OPEN_SPLICE_AND_CLOSE.byteValue());
       writeVarint64(output, openSpliceAndCloseReal.modeOffset);
       writeVarint64(output, openSpliceAndCloseReal.xattrOffset);
       writeVarint64(output, openSpliceAndCloseReal.size);
       writeVarint64(output, openSpliceAndCloseReal.offset);
+    } else if (this instanceof OpenSpliceAndCloseMeta openSpliceAndCloseMeta) {
+      output.write(DeltaOperationType.OPEN_SPLICE_AND_CLOSE.byteValue());
+      writeVarint64(output, openSpliceAndCloseMeta.size);
+      writeVarint64(output, openSpliceAndCloseMeta.offset);
     } else if (this instanceof Open open) {
       output.write(DeltaOperationType.OPEN.byteValue());
       writeVarint64(output, open.modeOffset);
@@ -130,16 +130,13 @@
    * @see #readVarint64
    */
   private static void writeVarint64(ByteArrayOutputStream output, long value) {
-    while (value != 0) {
+    do {
       byte b = (byte) (value & 0x7F);
       value >>= 7;
       if (value != 0) {
         b |= (byte) 0x80;
       }
       output.write(b);
-      if (value == 0) {
-        break;
-      }
-    }
+    } while (value != 0);
   }
 }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaPartPayload.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaPartPayload.java
index 31c192d..ccd5630 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaPartPayload.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaPartPayload.java
@@ -156,7 +156,7 @@
             Decoder.ofByteArray()
                 .map(
                     bytes -> parseDeltaOperationList(bytes, objectTypes),
-                    deltaOperations -> serializeDeltaOperationList(deltaOperations)))
+                    DeltaPartPayload::serializeDeltaOperationList))
         .contramap(DeltaPartPayload::decompress, DeltaPartPayload::compress);
   }
 }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaSuperblock.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaSuperblock.java
index 9513fa0..b8143af 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaSuperblock.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaSuperblock.java
@@ -5,16 +5,11 @@
 package eu.mulk.jgvariant.ostree;
 
 import eu.mulk.jgvariant.core.Decoder;
-import eu.mulk.jgvariant.core.Signature;
-import eu.mulk.jgvariant.core.Variant;
 import java.io.ByteArrayOutputStream;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * A static delta.
@@ -78,29 +73,7 @@
                       DeltaSuperblock::parseDeltaNameList, DeltaSuperblock::serializeDeltaNameList),
               Decoder.ofArray(DeltaMetaEntry.decoder()).withByteOrder(ByteOrder.LITTLE_ENDIAN),
               Decoder.ofArray(DeltaFallback.decoder()).withByteOrder(ByteOrder.LITTLE_ENDIAN))
-          .map(DeltaSuperblock::byteSwappedIfBigEndian, DeltaSuperblock::withSpecifiedByteOrder);
-
-  private DeltaSuperblock withSpecifiedByteOrder() {
-    Map<String, Variant> extendedMetadataMap = new HashMap<>(metadata().fields());
-
-    try {
-      extendedMetadataMap.putIfAbsent(
-          "ostree.endianness", new Variant(Signature.parse("y"), (byte) 'l'));
-    } catch (ParseException e) {
-      // impossible
-      throw new IllegalStateException(e);
-    }
-
-    return new DeltaSuperblock(
-        new Metadata(extendedMetadataMap),
-        timestamp,
-        fromChecksum,
-        toChecksum,
-        commit,
-        dependencies,
-        entries,
-        fallbacks);
-  }
+          .map(DeltaSuperblock::byteSwappedIfBigEndian, DeltaSuperblock::byteSwappedIfBigEndian);
 
   private DeltaSuperblock byteSwappedIfBigEndian() {
     // Fix up the endianness of the 'entries' and 'fallbacks' fields, which have
diff --git a/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderTest.java b/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderTest.java
index 75c5ea4..4465d02 100644
--- a/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderTest.java
+++ b/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderTest.java
@@ -12,18 +12,16 @@
 import eu.mulk.jgvariant.core.Signature;
 import eu.mulk.jgvariant.core.Variant;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.HexFormat;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import org.junit.jupiter.api.Test;
 
 @TestWithResources
 @SuppressWarnings({
+  "DoubleBraceInitialization",
   "ImmutableListOf1",
   "ImmutableMapOf1",
   "initialization.field.uninitialized",
-  "NullAway"
+  "NullAway",
 })
 class OstreeDecoderTest {
 
@@ -51,7 +49,8 @@
   @Test
   void summaryDecoder() {
     var decoder = Summary.decoder();
-    var summary = decoder.decode(ByteBuffer.wrap(summaryBytes));
+    var input = ByteBuffer.wrap(summaryBytes);
+    var summary = decoder.decode(input);
 
     assertAll(
         () ->
@@ -70,65 +69,102 @@
                 summary.entries()),
         () ->
             assertEquals(
-                Map.of(
-                    "ostree.summary.last-modified",
-                    new Variant(Signature.parse("t"), 1640537300L),
-                    "ostree.summary.tombstone-commits",
-                    new Variant(Signature.parse("b"), false),
-                    "ostree.static-deltas",
-                    new Variant(
-                        Signature.parse("a{sv}"),
-                        Map.of(
-                            "3d3b3329dca38871f29aeda1bf5854d76c707fa269759a899d0985c91815fe6f-66ff167ff35ce87daac817447a9490a262ee75f095f017716a6eb1a9d9eb3350",
-                            new Variant(
-                                Signature.parse("ay"),
-                                bytesOfHex(
-                                    "03738040e28e7662e9c9d2599c530ea974e642c9f87e6c00cbaa39a0cdac8d44")),
-                            "31c8835d5c9d2c6687a50091c85142d1b2d853ff416a9fb81b4ee30754510d52",
-                            new Variant(
-                                Signature.parse("ay"),
-                                bytesOfHex(
-                                    "f481144629474bd88c106e45ac405ebd75b324b0655af1aec14b31786ae1fd61")),
-                            "31c8835d5c9d2c6687a50091c85142d1b2d853ff416a9fb81b4ee30754510d52-3d3b3329dca38871f29aeda1bf5854d76c707fa269759a899d0985c91815fe6f",
-                            new Variant(
-                                Signature.parse("ay"),
-                                bytesOfHex(
-                                    "2c6a07bc1cf4d7ce7d00f82d7d2d6d156fd0e31d476851b46dc2306b181b064a")))),
-                    "ostree.summary.mode",
-                    new Variant(Signature.parse("s"), "bare"),
-                    "ostree.summary.indexed-deltas",
-                    new Variant(Signature.parse("b"), true)),
+                new LinkedHashMap<String, Variant>() {
+                  {
+                    put("ostree.summary.mode", new Variant(Signature.parse("s"), "bare"));
+                    put(
+                        "ostree.summary.last-modified",
+                        new Variant(Signature.parse("t"), 1640537300L));
+                    put(
+                        "ostree.summary.tombstone-commits",
+                        new Variant(Signature.parse("b"), false));
+                    put(
+                        "ostree.static-deltas",
+                        new Variant(
+                            Signature.parse("a{sv}"),
+                            new LinkedHashMap<String, Variant>() {
+                              {
+                                put(
+                                    "3d3b3329dca38871f29aeda1bf5854d76c707fa269759a899d0985c91815fe6f-66ff167ff35ce87daac817447a9490a262ee75f095f017716a6eb1a9d9eb3350",
+                                    new Variant(
+                                        Signature.parse("ay"),
+                                        bytesOfHex(
+                                            "03738040e28e7662e9c9d2599c530ea974e642c9f87e6c00cbaa39a0cdac8d44")));
+                                put(
+                                    "31c8835d5c9d2c6687a50091c85142d1b2d853ff416a9fb81b4ee30754510d52",
+                                    new Variant(
+                                        Signature.parse("ay"),
+                                        bytesOfHex(
+                                            "f481144629474bd88c106e45ac405ebd75b324b0655af1aec14b31786ae1fd61")));
+                                put(
+                                    "31c8835d5c9d2c6687a50091c85142d1b2d853ff416a9fb81b4ee30754510d52-3d3b3329dca38871f29aeda1bf5854d76c707fa269759a899d0985c91815fe6f",
+                                    new Variant(
+                                        Signature.parse("ay"),
+                                        bytesOfHex(
+                                            "2c6a07bc1cf4d7ce7d00f82d7d2d6d156fd0e31d476851b46dc2306b181b064a")));
+                              }
+                            }));
+                    put("ostree.summary.indexed-deltas", new Variant(Signature.parse("b"), true));
+                  }
+                },
                 summary.metadata().fields()));
 
+    var encoded = decoder.encode(summary);
+    input.rewind();
+    assertEquals(input, encoded);
+
     System.out.println(summary);
   }
 
   @Test
   void commitDecoder() {
     var decoder = Commit.decoder();
-    var commit = decoder.decode(ByteBuffer.wrap(commitBytes));
+    var input = ByteBuffer.wrap(commitBytes);
+    var commit = decoder.decode(input);
+
+    var encoded = decoder.encode(commit);
+    input.rewind();
+    assertEquals(input, encoded);
+
     System.out.println(commit);
   }
 
   @Test
   void dirTreeDecoder() {
     var decoder = DirTree.decoder();
-    var dirTree = decoder.decode(ByteBuffer.wrap(dirTreeBytes));
+    var input = ByteBuffer.wrap(dirTreeBytes);
+    var dirTree = decoder.decode(input);
+
+    var encoded = decoder.encode(dirTree);
+    input.rewind();
+    assertEquals(input, encoded);
+
     System.out.println(dirTree);
   }
 
   @Test
   void dirMetaDecoder() {
     var decoder = DirMeta.decoder();
+    var input = ByteBuffer.wrap(dirMetaBytes);
     var dirMeta = decoder.decode(ByteBuffer.wrap(dirMetaBytes));
+
+    var encoded = decoder.encode(dirMeta);
+    input.rewind();
+    assertEquals(input, encoded);
+
     System.out.println(dirMeta);
   }
 
   @Test
   void superblockDecoder() {
     var decoder = DeltaSuperblock.decoder();
-    var deltaSuperblock = decoder.decode(ByteBuffer.wrap(deltaSuperblockBytes));
+    var input = ByteBuffer.wrap(deltaSuperblockBytes);
+    var deltaSuperblock = decoder.decode(input);
     System.out.println(deltaSuperblock);
+
+    var encoded = decoder.encode(deltaSuperblock);
+    input.rewind();
+    assertEquals(input, encoded);
   }
 
   @Test
@@ -137,9 +173,12 @@
     var superblock = superblockDecoder.decode(ByteBuffer.wrap(deltaSuperblockBytes));
 
     var decoder = DeltaPartPayload.decoder(superblock.entries().get(0));
-    var deltaPartPayload = decoder.decode(ByteBuffer.wrap(deltaPartPayloadBytes));
+    var input = ByteBuffer.wrap(deltaPartPayloadBytes);
+    var deltaPartPayload = decoder.decode(input);
 
-    System.out.println(deltaPartPayload);
+    var encoded = decoder.encode(deltaPartPayload);
+    var decodedAgain = decoder.decode(encoded);
+    assertEquals(deltaPartPayload, decodedAgain);
   }
 
   private static List<Byte> bytesOfHex(String hex) {