Add more property-based tests and fix more bugs.

Change-Id: I8deb1a7d75078c037714541d8f6f656052c2476c
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 fcbb639..2121cfb 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
@@ -364,7 +364,7 @@
     // Determining the framing offset size requires trial and error.
     int framingOffsetSize;
     for (framingOffsetSize = 0;; framingOffsetSize = max(1, framingOffsetSize << 1)) {
-      if (elementsRelativeEnd + framingOffsetSize* framingOffsets.size() >= 1 << (8*framingOffsetSize)) {
+      if (elementsRelativeEnd + (long)framingOffsetSize * framingOffsets.size() >= 1L << (8*framingOffsetSize)) {
         continue;
       }
 
@@ -457,7 +457,7 @@
         }
 
         // Write the framing offsets.
-        int framingOffsetSize = computeFramingOffsetSize(byteWriter.position() - startOffset, framingOffsets);
+        int framingOffsetSize = max(1, computeFramingOffsetSize(byteWriter.position() - startOffset, framingOffsets));
         for (var framingOffset : framingOffsets) {
           byteWriter.writeIntN(framingOffset, framingOffsetSize);
         }
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 42b7056..bb31e50 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
@@ -130,6 +130,7 @@
    * @see #readVarint64
    */
   private static void writeVarint64(ByteArrayOutputStream output, long value) {
+    int n = 0;
     do {
       byte b = (byte) (value & 0x7F);
       value >>= 7;
@@ -137,6 +138,7 @@
         b |= (byte) 0x80;
       }
       output.write(b);
-    } while (value != 0);
+      ++n;
+    } while (value != 0 && n < 10);
   }
 }
diff --git a/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderPropertyTest.java b/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderPropertyTest.java
index acd11c4..da15ff2 100644
--- a/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderPropertyTest.java
+++ b/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/OstreeDecoderPropertyTest.java
@@ -1,6 +1,9 @@
 package eu.mulk.jgvariant.ostree;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
 import eu.mulk.jgvariant.core.Decoder;
+import java.util.List;
 import java.util.Map;
 import net.jqwik.api.*;
 
@@ -21,14 +24,88 @@
     }
   }
 
+  @Group
+  class DeltaSuperblockRoundtripLaw implements RoundtripLaw<DeltaSuperblock> {
+
+    @Override
+    public Decoder<DeltaSuperblock> decoder() {
+      return DeltaSuperblock.decoder();
+    }
+
+    @Override
+    public Arbitrary<DeltaSuperblock> anyT() {
+      return anyDeltaSuperblock();
+    }
+  }
+
+  @Group
+  @Disabled(
+      "Not implemented correctly: Requires enough file entries to parse all the delta operations.")
+  class DeltaPartPayloadRoundtripLaw implements RoundtripLaw<DeltaPartPayload> {
+
+    @Override
+    public Decoder<DeltaPartPayload> decoder() {
+      // FIXME
+      var deltaMetaEntry = new DeltaMetaEntry(0, Checksum.zero(), 0, 0, List.of());
+      return DeltaPartPayload.decoder(deltaMetaEntry);
+    }
+
+    @Override
+    public Arbitrary<DeltaPartPayload> anyT() {
+      return anyDeltaPartPayload();
+    }
+  }
+
+  @Group
+  class DirTreeRoundtripLaw implements RoundtripLaw<DirTree> {
+
+    @Override
+    public Decoder<DirTree> decoder() {
+      return DirTree.decoder();
+    }
+
+    @Override
+    public Arbitrary<DirTree> anyT() {
+      return anyDirTree();
+    }
+  }
+
+  @Group
+  class DirMetaRoundtripLaw implements RoundtripLaw<DirMeta> {
+
+    @Override
+    public Decoder<DirMeta> decoder() {
+      return DirMeta.decoder();
+    }
+
+    @Override
+    public Arbitrary<DirMeta> anyT() {
+      return anyDirMeta();
+    }
+  }
+
+  @Group
+  class CommitRoundtripLaw implements RoundtripLaw<Commit> {
+
+    @Override
+    public Decoder<Commit> decoder() {
+      return Commit.decoder();
+    }
+
+    @Override
+    public Arbitrary<Commit> anyT() {
+      return anyCommit();
+    }
+  }
+
   interface RoundtripLaw<T> {
 
     @Property
-    default boolean roundtripsWell(@ForAll(value = "anyT") T entityLeft) {
+    default void roundtripsWell(@ForAll(value = "anyT") T entityLeft) {
       var decoder = decoder();
       var bytes = decoder.encode(entityLeft);
       var entityRight = decoder.decode(bytes);
-      return entityLeft.equals(entityRight);
+      assertEquals(entityLeft, entityRight);
     }
 
     Decoder<T> decoder();
@@ -61,6 +138,148 @@
 
   @Provide
   Arbitrary<Checksum> anyChecksum() {
-    return Arbitraries.of(new Checksum(new ByteString(new byte[32])));
+    return Arbitraries.bytes()
+        .array(byte[].class)
+        .ofSize(32)
+        .map(ByteString::new)
+        .map(Checksum::new);
+  }
+
+  @Provide
+  Arbitrary<DeltaSuperblock> anyDeltaSuperblock() {
+    return Combinators.combine(
+            anyMetadata(),
+            Arbitraries.longs(),
+            anyChecksum(),
+            anyChecksum(),
+            anyCommit(),
+            anyDeltaName().list(),
+            anyDeltaMetaEntry().list(),
+            anyDeltaFallback().list())
+        .as(DeltaSuperblock::new);
+  }
+
+  @Provide
+  Arbitrary<DeltaPartPayload> anyDeltaPartPayload() {
+    return Combinators.combine(
+            anyFileMode().list(),
+            anyXattr().list().list(),
+            anyByteString(),
+            anyDeltaOperation().list())
+        .as(DeltaPartPayload::new);
+  }
+
+  @Provide
+  Arbitrary<DeltaOperation> anyDeltaOperation() {
+    return Arbitraries.oneOf(
+        Combinators.combine(Arbitraries.longs(), Arbitraries.longs())
+            .as(DeltaOperation.OpenSpliceAndCloseMeta::new),
+        Combinators.combine(
+                Arbitraries.longs(), Arbitraries.longs(), Arbitraries.longs(), Arbitraries.longs())
+            .as(DeltaOperation.OpenSpliceAndCloseReal::new),
+        Combinators.combine(Arbitraries.longs(), Arbitraries.longs(), Arbitraries.longs())
+            .as(DeltaOperation.Open::new),
+        Combinators.combine(Arbitraries.longs(), Arbitraries.longs()).as(DeltaOperation.Write::new),
+        Arbitraries.longs().map(DeltaOperation.SetReadSource::new),
+        Arbitraries.of(new DeltaOperation.UnsetReadSource()),
+        Arbitraries.of(new DeltaOperation.Close()),
+        Combinators.combine(Arbitraries.longs(), Arbitraries.longs())
+            .as(DeltaOperation.BsPatch::new));
+  }
+
+  @Provide
+  Arbitrary<DeltaPartPayload.FileMode> anyFileMode() {
+    return Combinators.combine(
+            Arbitraries.integers(), Arbitraries.integers(), Arbitraries.integers())
+        .as(DeltaPartPayload.FileMode::new);
+  }
+
+  @Provide
+  Arbitrary<Xattr> anyXattr() {
+    return Combinators.combine(anyByteString(), anyByteString()).as(Xattr::new);
+  }
+
+  @Provide
+  Arbitrary<ByteString> anyByteString() {
+    return Arbitraries.bytes().array(byte[].class).map(ByteString::new);
+  }
+
+  @Provide
+  Arbitrary<DirTree> anyDirTree() {
+    return Combinators.combine(anyDirTreeFile().list(), anyDirTreeDirectory().list())
+        .as(DirTree::new);
+  }
+
+  @Provide
+  Arbitrary<DirMeta> anyDirMeta() {
+    return Combinators.combine(
+            Arbitraries.integers(),
+            Arbitraries.integers(),
+            Arbitraries.integers(),
+            anyXattr().list())
+        .as(DirMeta::new);
+  }
+
+  @Provide
+  Arbitrary<Commit> anyCommit() {
+    return Combinators.combine(
+            anyMetadata(),
+            anyChecksum(),
+            anyRelatedObject().list(),
+            Arbitraries.strings(),
+            Arbitraries.strings(),
+            Arbitraries.longs(),
+            anyChecksum(),
+            anyChecksum())
+        .as(Commit::new);
+  }
+
+  @Provide
+  Arbitrary<Commit.RelatedObject> anyRelatedObject() {
+    return Combinators.combine(Arbitraries.strings(), anyChecksum()).as(Commit.RelatedObject::new);
+  }
+
+  @Provide
+  Arbitrary<DeltaSuperblock.DeltaName> anyDeltaName() {
+    return Combinators.combine(anyChecksum(), anyChecksum()).as(DeltaSuperblock.DeltaName::new);
+  }
+
+  @Provide
+  Arbitrary<DeltaMetaEntry> anyDeltaMetaEntry() {
+    return Combinators.combine(
+            Arbitraries.integers(),
+            anyChecksum(),
+            Arbitraries.longs(),
+            Arbitraries.longs(),
+            anyDeltaObject().list())
+        .as(DeltaMetaEntry::new);
+  }
+
+  @Provide
+  Arbitrary<DeltaMetaEntry.DeltaObject> anyDeltaObject() {
+    return Combinators.combine(anyObjectType(), anyChecksum()).as(DeltaMetaEntry.DeltaObject::new);
+  }
+
+  @Provide
+  Arbitrary<ObjectType> anyObjectType() {
+    return Arbitraries.of(ObjectType.values());
+  }
+
+  @Provide
+  Arbitrary<DeltaFallback> anyDeltaFallback() {
+    return Combinators.combine(
+            anyObjectType(), anyChecksum(), Arbitraries.longs(), Arbitraries.longs())
+        .as(DeltaFallback::new);
+  }
+
+  @Provide
+  Arbitrary<DirTree.Directory> anyDirTreeDirectory() {
+    return Combinators.combine(Arbitraries.strings(), anyChecksum(), anyChecksum())
+        .as(DirTree.Directory::new);
+  }
+
+  @Provide
+  Arbitrary<DirTree.File> anyDirTreeFile() {
+    return Combinators.combine(Arbitraries.strings(), anyChecksum()).as(DirTree.File::new);
   }
 }