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);
}
}