jvgariant-ostree: Correctly deserialize delta operations.

Change-Id: Ic6659d7ea5e9411220571c33979e29471cec897e
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 c1f2701..73cb67c 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
@@ -1,53 +1,85 @@
 package eu.mulk.jgvariant.ostree;
 
-import static org.apiguardian.api.API.Status.STABLE;
-
-import org.apiguardian.api.API;
+import java.nio.ByteBuffer;
 
 /** An operation in a static delta. */
-@API(status = STABLE)
-public enum DeltaOperation {
-  OPEN_SPLICE_AND_CLOSE((byte) 'S'),
-  OPEN((byte) 'o'),
-  WRITE((byte) 'w'),
-  SET_READ_SOURCE((byte) 'r'),
-  UNSET_READ_SOURCE((byte) 'R'),
-  CLOSE((byte) 'c'),
-  BSPATCH((byte) 'B');
+public sealed interface DeltaOperation {
 
-  private final byte byteValue;
+  record OpenSpliceAndCloseMeta(long offset, long size) implements DeltaOperation {}
 
-  DeltaOperation(byte byteValue) {
-    this.byteValue = byteValue;
-  }
+  record OpenSpliceAndCloseReal(long offset, long size, long modeOffset, long xattrOffset)
+      implements DeltaOperation {}
 
-  /**
-   * The serialized byte value.
-   *
-   * @return a serialized byte value for use in GVariant structures.
-   */
-  public byte byteValue() {
-    return byteValue;
-  }
+  record Open(long size, long modeOffset, long xattrOffset) implements DeltaOperation {}
 
-  /**
-   * Returns the {@link DeltaOperation} corresponding to a serialized GVariant value.
-   *
-   * @param byteValue a serialized value as used in GVariant.
-   * @return the {@link DeltaOperation} corresponding to the serialized value.
-   * @throws IllegalArgumentException if the byte value is invalid.
-   */
-  public static DeltaOperation valueOf(byte byteValue) {
-    return switch (byteValue) {
-      case (byte) 'S' -> OPEN_SPLICE_AND_CLOSE;
-      case (byte) 'o' -> OPEN;
-      case (byte) 'w' -> WRITE;
-      case (byte) 'r' -> SET_READ_SOURCE;
-      case (byte) 'R' -> UNSET_READ_SOURCE;
-      case (byte) 'c' -> CLOSE;
-      case (byte) 'B' -> BSPATCH;
-      default -> throw new IllegalArgumentException(
-          "invalid DeltaOperation: %d".formatted(byteValue));
+  record Write(long offset, long size) implements DeltaOperation {}
+
+  record SetReadSource(long offset) implements DeltaOperation {}
+
+  record UnsetReadSource() implements DeltaOperation {}
+
+  record Close() implements DeltaOperation {}
+
+  record BsPatch(long offset, long size) implements DeltaOperation {}
+
+  static DeltaOperation readFrom(ByteBuffer byteBuffer, ObjectType objectType) {
+    byte opcode = byteBuffer.get();
+    return switch (DeltaOperationType.valueOf(opcode)) {
+      case OPEN_SPLICE_AND_CLOSE -> {
+        if (objectType == ObjectType.FILE || objectType == ObjectType.PAYLOAD_LINK) {
+          long modeOffset = readVarint64(byteBuffer);
+          long xattrOffset = readVarint64(byteBuffer);
+          long size = readVarint64(byteBuffer);
+          long offset = readVarint64(byteBuffer);
+          yield new OpenSpliceAndCloseReal(offset, size, modeOffset, xattrOffset);
+        } else {
+          long size = readVarint64(byteBuffer);
+          long offset = readVarint64(byteBuffer);
+          yield new OpenSpliceAndCloseMeta(offset, size);
+        }
+      }
+      case OPEN -> {
+        long modeOffset = readVarint64(byteBuffer);
+        long xattrOffset = readVarint64(byteBuffer);
+        long size = readVarint64(byteBuffer);
+        yield new Open(size, modeOffset, xattrOffset);
+      }
+      case WRITE -> {
+        long size = readVarint64(byteBuffer);
+        long offset = readVarint64(byteBuffer);
+        yield new Write(offset, size);
+      }
+      case SET_READ_SOURCE -> {
+        long offset = readVarint64(byteBuffer);
+        yield new SetReadSource(offset);
+      }
+      case UNSET_READ_SOURCE -> new UnsetReadSource();
+      case CLOSE -> new Close();
+      case BSPATCH -> {
+        long offset = readVarint64(byteBuffer);
+        long size = readVarint64(byteBuffer);
+        yield new BsPatch(offset, size);
+      }
     };
   }
+
+  /**
+   * Reads a Protobuf varint from a byte buffer.
+   *
+   * <p>Varint64 encoding is little-endian and works by using the lower 7 bits of each byte as the
+   * payload and the 0x80 bit as an indicator of whether the varint continues.
+   */
+  private static long readVarint64(ByteBuffer byteBuffer) {
+    long acc = 0L;
+
+    for (int i = 0; i < 10; ++i) {
+      long b = byteBuffer.get();
+      acc |= (b & 0x7F) << (i * 7);
+      if ((b & 0x80) == 0) {
+        break;
+      }
+    }
+
+    return acc;
+  }
 }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperationType.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperationType.java
new file mode 100644
index 0000000..ac2056c
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperationType.java
@@ -0,0 +1,47 @@
+package eu.mulk.jgvariant.ostree;
+
+enum DeltaOperationType {
+  OPEN_SPLICE_AND_CLOSE((byte) 'S'),
+  OPEN((byte) 'o'),
+  WRITE((byte) 'w'),
+  SET_READ_SOURCE((byte) 'r'),
+  UNSET_READ_SOURCE((byte) 'R'),
+  CLOSE((byte) 'c'),
+  BSPATCH((byte) 'B');
+
+  private final byte byteValue;
+
+  DeltaOperationType(byte byteValue) {
+    this.byteValue = byteValue;
+  }
+
+  /**
+   * The serialized byte value.
+   *
+   * @return a serialized byte value for use in GVariant structures.
+   */
+  byte byteValue() {
+    return byteValue;
+  }
+
+  /**
+   * Returns the {@link DeltaOperationType} corresponding to a serialized GVariant value.
+   *
+   * @param byteValue a serialized value as used in GVariant.
+   * @return the {@link DeltaOperationType} corresponding to the serialized value.
+   * @throws IllegalArgumentException if the byte value is invalid.
+   */
+  static DeltaOperationType valueOf(byte byteValue) {
+    return switch (byteValue) {
+      case (byte) 'S' -> OPEN_SPLICE_AND_CLOSE;
+      case (byte) 'o' -> OPEN;
+      case (byte) 'w' -> WRITE;
+      case (byte) 'r' -> SET_READ_SOURCE;
+      case (byte) 'R' -> UNSET_READ_SOURCE;
+      case (byte) 'c' -> CLOSE;
+      case (byte) 'B' -> BSPATCH;
+      default -> throw new IllegalArgumentException(
+          "invalid DeltaOperation: %d".formatted(byteValue));
+    };
+  }
+}
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 ed0f4ff..4e2587f 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
@@ -7,6 +7,7 @@
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.ArrayList;
 import java.util.List;
 import org.tukaani.xz.XZInputStream;
 
@@ -31,15 +32,6 @@
     ByteString rawDataSource,
     List<DeltaOperation> operations) {
 
-  private static final Decoder<DeltaPartPayload> DECODER =
-      Decoder.ofStructure(
-              DeltaPartPayload.class,
-              Decoder.ofArray(FileMode.decoder()),
-              Decoder.ofArray(Decoder.ofArray(Xattr.decoder())),
-              ByteString.decoder(),
-              ByteString.decoder().map(DeltaPartPayload::parseDeltaOperationList))
-          .contramap(DeltaPartPayload::preparse);
-
   private static ByteBuffer preparse(ByteBuffer byteBuffer) {
     byte compressionByte = byteBuffer.get(0);
     var dataSlice = byteBuffer.slice(1, byteBuffer.limit() - 1);
@@ -65,25 +57,40 @@
     };
   }
 
-  private static List<DeltaOperation> parseDeltaOperationList(ByteString byteString) {
-    return byteString.stream().map(DeltaOperation::valueOf).toList();
+  private static List<DeltaOperation> parseDeltaOperationList(
+      ByteString byteString, List<ObjectType> objectTypes) {
+    List<DeltaOperation> deltaOperations = new ArrayList<>();
+    var byteBuffer = ByteBuffer.wrap(byteString.bytes());
+    int objectIndex = 0;
+
+    while (byteBuffer.hasRemaining()) {
+      var currentOperation = DeltaOperation.readFrom(byteBuffer, objectTypes.get(objectIndex));
+      deltaOperations.add(currentOperation);
+      if (currentOperation instanceof DeltaOperation.Close
+          || currentOperation instanceof DeltaOperation.OpenSpliceAndCloseMeta
+          || currentOperation instanceof DeltaOperation.OpenSpliceAndCloseReal) {
+        ++objectIndex;
+      }
+    }
+
+    return deltaOperations;
   }
 
   /**
    * A file mode triple (UID, GID, and permission bits).
    *
-   * @param uid
-   * @param gid
-   * @param mode
+   * @param uid the user ID that owns the file.
+   * @param gid the group ID that owns the file.
+   * @param mode the POSIX permission bits.
    */
   public record FileMode(int uid, int gid, int mode) {
 
     private static final Decoder<FileMode> DECODER =
         Decoder.ofStructure(
             FileMode.class,
-            Decoder.ofInt().withByteOrder(ByteOrder.LITTLE_ENDIAN),
-            Decoder.ofInt().withByteOrder(ByteOrder.LITTLE_ENDIAN),
-            Decoder.ofInt().withByteOrder(ByteOrder.LITTLE_ENDIAN));
+            Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+            Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+            Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN));
 
     /**
      * Acquires a {@link Decoder} for the enclosing type.
@@ -98,12 +105,18 @@
   /**
    * Acquires a {@link Decoder} for the enclosing type.
    *
-   * <p>FIXME: The first byte is actually a compression byte: {@code 0} for none, {@code 'x'} for
-   * LZMA.
-   *
    * @return a possibly shared {@link Decoder}.
    */
-  public static Decoder<DeltaPartPayload> decoder() {
-    return DECODER;
+  public static Decoder<DeltaPartPayload> decoder(DeltaMetaEntry deltaMetaEntry) {
+    var objectTypes =
+        deltaMetaEntry.objects().stream().map(DeltaMetaEntry.DeltaObject::objectType).toList();
+    return Decoder.ofStructure(
+            DeltaPartPayload.class,
+            Decoder.ofArray(FileMode.decoder()),
+            Decoder.ofArray(Decoder.ofArray(Xattr.decoder())),
+            ByteString.decoder(),
+            ByteString.decoder()
+                .map(byteString -> parseDeltaOperationList(byteString, objectTypes)))
+        .contramap(DeltaPartPayload::preparse);
   }
 }
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 05da7ed..db222f9 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
@@ -11,7 +11,6 @@
 import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.Map;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 @TestWithResources
@@ -94,11 +93,14 @@
     System.out.println(deltaSuperblock);
   }
 
-  @Disabled("invalid: compression byte not taken into account")
   @Test
   void testPartPayloadDecoder() {
-    var decoder = DeltaPartPayload.decoder();
+    var superblockDecoder = DeltaSuperblock.decoder();
+    var superblock = superblockDecoder.decode(ByteBuffer.wrap(deltaSuperblockBytes));
+
+    var decoder = DeltaPartPayload.decoder(superblock.entries().get(0));
     var deltaPartPayload = decoder.decode(ByteBuffer.wrap(deltaPartPayloadBytes));
+
     System.out.println(deltaPartPayload);
   }
 }