Properly document jgvariant-ostree.

Change-Id: I0aa3b1df512ef99d0e25d73efdd34a1b488e7d0d
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ByteString.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ByteString.java
index 1a85547..cf6e99a 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ByteString.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ByteString.java
@@ -3,15 +3,24 @@
 import eu.mulk.jgvariant.core.Decoder;
 import java.util.Arrays;
 import java.util.HexFormat;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 /**
  * A wrapper for a {@code byte[]} that implements {@link #equals(Object)}, {@link #hashCode()}, and
  * {@link #toString()} according to value semantics.
+ *
+ * @param bytes the byte array that this ByteString wraps.
  */
 public record ByteString(byte[] bytes) {
 
   private static final Decoder<ByteString> DECODER = Decoder.ofByteArray().map(ByteString::new);
 
+  /**
+   * Returns a decoder for a {@code byte[]} that wraps the result in {@link ByteString}.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<ByteString> decoder() {
     return DECODER;
   }
@@ -31,11 +40,42 @@
     return "ByteString{hex=\"%s\"}".formatted(hex());
   }
 
+  /**
+   * Converts the contained byte array into a hex string.
+   *
+   * <p>Useful for printing.
+   *
+   * @return a hex string representation of this byte string.
+   */
   public String hex() {
     return HexFormat.of().formatHex(bytes);
   }
 
+  /**
+   * Parses a hex string into a {@link ByteString}.
+   *
+   * @param hex a hex string.
+   * @return a {@link ByteString} corresponding to the given hex string.
+   */
   public static ByteString ofHex(String hex) {
     return new ByteString(HexFormat.of().parseHex(hex));
   }
+
+  /**
+   * Returns the number of bytes in the byte string.
+   *
+   * @return the number of bytes in the byte string.
+   */
+  public int size() {
+    return bytes.length;
+  }
+
+  /**
+   * Returns a {@link Stream} of all the bytes in the byte string.
+   *
+   * @return a new {@link Stream}.
+   */
+  Stream<Byte> stream() {
+    return IntStream.range(0, bytes.length).mapToObj(i -> bytes[i]);
+  }
 }
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 f77eb57..705e27c 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
@@ -1,24 +1,85 @@
 package eu.mulk.jgvariant.ostree;
 
 import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteBuffer;
 
 /**
  * A wrapper for {@link ByteString} that refers to a content-addressed object in an OSTree
  * repository.
+ *
+ * @param byteString the bytes that make up this {@link Checksum}.
  */
-public record Checksum(ByteString bytes) {
+public record Checksum(ByteString byteString) {
+
+  private static final int SIZE = 32;
 
   private static final Decoder<Checksum> DECODER = ByteString.decoder().map(Checksum::new);
 
+  public Checksum {
+    if (byteString.size() != SIZE) {
+      throw new IllegalArgumentException(
+          "attempted to construct Checksum of length %d (expected: %d)"
+              .formatted(byteString.size(), SIZE));
+    }
+  }
+
+  /**
+   * A decoder for a {@code byte[]} that wraps the result in a {@link Checksum}.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<Checksum> decoder() {
     return DECODER;
   }
 
-  public String hex() {
-    return bytes.hex();
+  /**
+   * Returns an empty checksum.
+   *
+   * @return a checksum whose bits are all zero.
+   */
+  public static Checksum zero() {
+    return new Checksum(new ByteString(new byte[SIZE]));
   }
 
+  /**
+   * Checks whether the checksum contains only zero bits.
+   *
+   * @return {@code true} if the byte string is equal to {@link #zero()}, {@code false} otherwise.
+   */
+  public boolean isEmpty() {
+    return equals(zero());
+  }
+
+  /**
+   * Converts the contained byte array into a hex string.
+   *
+   * <p>Useful for printing.
+   *
+   * @return a hex string representation of the bytes making up this checksum.
+   */
+  public String hex() {
+    return byteString.hex();
+  }
+
+  /**
+   * Parses a hex string into a {@link Checksum}.
+   *
+   * @param hex a hex string.
+   * @return a {@link Checksum} corresponding to the given hex string.
+   */
   public static Checksum ofHex(String hex) {
     return new Checksum(ByteString.ofHex(hex));
   }
+
+  /**
+   * Reads a Checksum for a {@link ByteBuffer}.
+   *
+   * @param byteBuffer the byte buffer to read from.
+   * @return a checksum.
+   */
+  public static Checksum readFrom(ByteBuffer byteBuffer) {
+    var bytes = new byte[SIZE];
+    byteBuffer.get(bytes);
+    return new Checksum(new ByteString(bytes));
+  }
 }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Commit.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Commit.java
index 43909ba..a250937 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Commit.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Commit.java
@@ -11,6 +11,19 @@
  * <p>Has an optional parent, a root directory, and various metadata.
  *
  * <p>Reference: {@code ostree-core.h#OSTREE_COMMIT_GVARIANT_STRING}
+ *
+ * @param metadata arbitrary metadata supplied by the user who made the commit.
+ * @param parentChecksum a (possibly {@link Checksum#isEmpty()}) reference to this commit's parent
+ *     commit.
+ * @param relatedObjects references to related commits.
+ * @param subject the subject line part of the commit message.
+ * @param body the body part of the commit message.
+ * @param timestamp UNIX epoch seconds of when the commit was done.
+ * @param rootDirTreeChecksum the checksum of the {@link DirTree} file describing the root
+ *     directory.
+ * @param rootDirMetaChecksum the checksum of the {@link DirMeta} file describing the root
+ *     directory.
+ * @see ObjectType#COMMIT
  */
 public record Commit(
     Metadata metadata,
@@ -22,6 +35,12 @@
     Checksum rootDirTreeChecksum,
     Checksum rootDirMetaChecksum) {
 
+  /**
+   * A reference to a related commit.
+   *
+   * @param ref the name of the reference.
+   * @param commitChecksum the checksum of the related commit.
+   */
   public record RelatedObject(String ref, Checksum commitChecksum) {
 
     private static final Decoder<RelatedObject> DECODER =
@@ -45,6 +64,11 @@
           Checksum.decoder(),
           Checksum.decoder());
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<Commit> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaFallback.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaFallback.java
index 1b3cc0a..46470d4 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaFallback.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaFallback.java
@@ -6,19 +6,31 @@
 /**
  * A fallback entry in a {@link DeltaSuperblock}.
  *
+ * <p>References a file in the OSTree repository.
+ *
  * <p>Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_FALLBACK_FORMAT}
+ *
+ * @param objectType the object type of the file represented by this fallback entry.
+ * @param checksum the checksum of the file represented by this fallback entry.
+ * @param compressedSize the compressed size of the file represented by this fallback entry.
+ * @param uncompressedSize the uncompressed size of the file represented by this fallback entry.
  */
 public record DeltaFallback(
-    byte objectType, Checksum checksum, long compressedSize, long uncompressedSize) {
+    ObjectType objectType, Checksum checksum, long compressedSize, long uncompressedSize) {
 
   private static final Decoder<DeltaFallback> DECODER =
       Decoder.ofStructure(
           DeltaFallback.class,
-          Decoder.ofByte(),
+          Decoder.ofByte().map(ObjectType::valueOf),
           Checksum.decoder(),
           Decoder.ofLong().withByteOrder(ByteOrder.LITTLE_ENDIAN), // FIXME: non-canonical
           Decoder.ofLong().withByteOrder(ByteOrder.LITTLE_ENDIAN)); // FIXME: non-canonical
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<DeltaFallback> decoder() {
     return DECODER;
   }
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 39ada86..0eaea73 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
@@ -1,22 +1,46 @@
 package eu.mulk.jgvariant.ostree;
 
 import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
  * An entry in a {@link DeltaSuperblock}.
  *
  * <p>Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_META_ENTRY_FORMAT}
+ *
+ * @param version the version corresponding to the version of {@link DeltaPartPayload}; always 0.
+ * @param checksum the checksum of the {@link DeltaPartPayload}.
+ * @param compressedSize the total compressed size of the delta.
+ * @param uncompressedSize the total uncompressed size of the files generated by the delta.
+ * @param objects a list of objects generated by the delta payload.
  */
 public record DeltaMetaEntry(
-    int version, Checksum checksum, long size, long usize, List<DeltaObject> objects) {
+    int version,
+    Checksum checksum,
+    long compressedSize,
+    long uncompressedSize,
+    List<DeltaObject> objects) {
 
-  record DeltaObject(byte objectType, Checksum checksum) {
+  /**
+   * A reference to an object generated by a {@link DeltaPartPayload}.
+   *
+   * @param objectType the file type.
+   * @param checksum the checksum of the resulting file.
+   */
+  public record DeltaObject(ObjectType objectType, Checksum checksum) {
 
     private static final Decoder<DeltaObject> DECODER =
-        Decoder.ofStructure(DeltaObject.class, Decoder.ofByte(), Checksum.decoder());
+        Decoder.ofStructure(
+            DeltaObject.class, Decoder.ofByte().map(ObjectType::valueOf), Checksum.decoder());
 
+    /**
+     * Acquires a {@link Decoder} for the enclosing type.
+     *
+     * @return a possibly shared {@link Decoder}.
+     */
     public static Decoder<DeltaObject> decoder() {
       return DECODER;
     }
@@ -29,9 +53,26 @@
           Checksum.decoder(),
           Decoder.ofLong().withByteOrder(ByteOrder.LITTLE_ENDIAN), // FIXME: non-canonical
           Decoder.ofLong().withByteOrder(ByteOrder.LITTLE_ENDIAN), // FIXME: non-canonical
-          Decoder.ofByteArray().map(x -> List.of()) // FIXME
-          );
+          Decoder.ofByteArray().map(DeltaMetaEntry::parseObjectList));
 
+  private static List<DeltaObject> parseObjectList(byte[] bytes) {
+    var byteBuffer = ByteBuffer.wrap(bytes);
+    List<DeltaObject> objects = new ArrayList<>();
+
+    while (byteBuffer.hasRemaining()) {
+      var type = ObjectType.valueOf(byteBuffer.get());
+      var checksum = Checksum.readFrom(byteBuffer);
+      objects.add(new DeltaObject(type, checksum));
+    }
+
+    return objects;
+  }
+
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<DeltaMetaEntry> decoder() {
     return DECODER;
   }
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
new file mode 100644
index 0000000..c1f2701
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaOperation.java
@@ -0,0 +1,53 @@
+package eu.mulk.jgvariant.ostree;
+
+import static org.apiguardian.api.API.Status.STABLE;
+
+import org.apiguardian.api.API;
+
+/** 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');
+
+  private final byte byteValue;
+
+  DeltaOperation(byte byteValue) {
+    this.byteValue = byteValue;
+  }
+
+  /**
+   * The serialized byte value.
+   *
+   * @return a serialized byte value for use in GVariant structures.
+   */
+  public byte byteValue() {
+    return byteValue;
+  }
+
+  /**
+   * 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));
+    };
+  }
+}
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 c8c5fe7..7f7dd23 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
@@ -5,13 +5,25 @@
 import java.util.List;
 
 /**
- * Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_PART_PAYLOAD_FORMAT_V0}
+ * A payload file from a static delta.
+ *
+ * <p>The first byte is a compression byte: {@code 0} for none, {@code 'x'} for LZMA. The actual
+ * GVariant data begins right after.
+ *
+ * <p>Reference: {@code
+ * ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_PART_PAYLOAD_FORMAT_V0}
+ *
+ * @param fileModes the {@link FileMode}s of the files generated by this delta payload.
+ * @param xattrs the {@link Xattr}s of the files generated by this delta payload.
+ * @param rawDataSource the data bytes used in the delta operations.
+ * @param operations the operations to apply during delta patching.
+ * @see DeltaSuperblock
  */
 public record DeltaPartPayload(
     List<FileMode> fileModes,
     List<List<Xattr>> xattrs,
     ByteString rawDataSource,
-    ByteString operations) {
+    List<DeltaOperation> operations) {
 
   private static final Decoder<DeltaPartPayload> DECODER =
       Decoder.ofStructure(
@@ -19,8 +31,19 @@
           Decoder.ofArray(FileMode.decoder()),
           Decoder.ofArray(Decoder.ofArray(Xattr.decoder())),
           ByteString.decoder(),
-          ByteString.decoder());
+          ByteString.decoder().map(DeltaPartPayload::parseDeltaOperationList));
 
+  private static List<DeltaOperation> parseDeltaOperationList(ByteString byteString) {
+    return byteString.stream().map(DeltaOperation::valueOf).toList();
+  }
+
+  /**
+   * A file mode triple (UID, GID, and permission bits).
+   *
+   * @param uid
+   * @param gid
+   * @param mode
+   */
   public record FileMode(int uid, int gid, int mode) {
 
     private static final Decoder<FileMode> DECODER =
@@ -30,11 +53,24 @@
             Decoder.ofInt().withByteOrder(ByteOrder.LITTLE_ENDIAN),
             Decoder.ofInt().withByteOrder(ByteOrder.LITTLE_ENDIAN));
 
+    /**
+     * Acquires a {@link Decoder} for the enclosing type.
+     *
+     * @return a possibly shared {@link Decoder}.
+     */
     public static Decoder<FileMode> decoder() {
       return DECODER;
     }
   }
 
+  /**
+   * 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;
   }
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 2b09645..edcf2bf 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
@@ -1,10 +1,27 @@
 package eu.mulk.jgvariant.ostree;
 
 import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.ArrayList;
 import java.util.List;
 
-/** Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_SUPERBLOCK_FORMAT} */
+/**
+ * A static delta.
+ *
+ * <p>Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_SUPERBLOCK_FORMAT}
+ *
+ * @param metadata arbitrary user-supplied metadata.
+ * @param timestamp UNIX epoch seconds of when the commit was done.
+ * @param fromChecksum a (possibly {@link Checksum#isEmpty()}) reference to the starting commit.
+ * @param toChecksum a (non-{@link Checksum#isEmpty()}) reference to the end commit.
+ * @param commit the commit metadata of the end commit.
+ * @param dependencies a list of other {@link DeltaSuperblock}s that need to be applied before this
+ *     one.
+ * @param entries a list of metadata on the {@link DeltaPartPayload}s that make up the delta.
+ * @param fallbacks a list of objects included in the delta as plain files that have to be fetched
+ *     separately.
+ */
 public record DeltaSuperblock(
     Metadata metadata,
     long timestamp,
@@ -15,11 +32,24 @@
     List<DeltaMetaEntry> entries,
     List<DeltaFallback> fallbacks) {
 
+  /**
+   * A specifier for another static delta.
+   *
+   * <p>Used to specify {@link DeltaSuperblock#dependencies()}.
+   *
+   * @param fromChecksum the {@link DeltaSuperblock#fromChecksum()} of the referenced delta.
+   * @param toChecksum the {@link DeltaSuperblock#toChecksum()} of the referenced delta.
+   */
   public record DeltaName(Checksum fromChecksum, Checksum toChecksum) {
 
     private static final Decoder<DeltaName> DECODER =
         Decoder.ofStructure(DeltaName.class, Checksum.decoder(), Checksum.decoder());
 
+    /**
+     * Acquires a {@link Decoder} for the enclosing type.
+     *
+     * @return a possibly shared {@link Decoder}.
+     */
     public static Decoder<DeltaName> decoder() {
       return DECODER;
     }
@@ -33,10 +63,28 @@
           Checksum.decoder(),
           Checksum.decoder(),
           Commit.decoder(),
-          Decoder.ofByteArray().map(x -> List.of()), // FIXME
+          Decoder.ofByteArray().map(DeltaSuperblock::parseDeltaNameList),
           Decoder.ofArray(DeltaMetaEntry.decoder()),
           Decoder.ofArray(DeltaFallback.decoder()));
 
+  private static List<DeltaName> parseDeltaNameList(byte[] bytes) {
+    var byteBuffer = ByteBuffer.wrap(bytes);
+    List<DeltaName> deltaNames = new ArrayList<>();
+
+    while (byteBuffer.hasRemaining()) {
+      var fromChecksum = Checksum.readFrom(byteBuffer);
+      var toChecksum = Checksum.readFrom(byteBuffer);
+      deltaNames.add(new DeltaName(fromChecksum, toChecksum));
+    }
+
+    return deltaNames;
+  }
+
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<DeltaSuperblock> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirMeta.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirMeta.java
index 3a9672d..35c5212 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirMeta.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirMeta.java
@@ -7,7 +7,19 @@
 /**
  * Permission bits and extended attributes for a directory.
  *
+ * <p>Often comes in a pair with {@link DirTree}.
+ *
+ * <p>Referenced by {@link Commit#rootDirMetaChecksum()} and {@link
+ * DirTree.Directory#dirChecksum()}.
+ *
  * <p>Reference: {@code ostree-core.h#OSTREE_DIRMETA_GVARIANT_STRING}
+ *
+ * @param uid the user ID that owns the directory.
+ * @param gid the group ID that owns the directory.
+ * @param mode the POSIX permission bits.
+ * @param xattrs POSIX extended attributes.
+ * @see DirTree
+ * @see ObjectType#DIR_META
  */
 public record DirMeta(int uid, int gid, int mode, List<Xattr> xattrs) {
 
@@ -19,6 +31,11 @@
           Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
           Decoder.ofArray(Xattr.decoder()));
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<DirMeta> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirTree.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirTree.java
index 3a14abb..ef7d05f 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirTree.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirTree.java
@@ -7,24 +7,49 @@
 /**
  * Metadata describing files and directories of a file tree.
  *
+ * <p>Often comes in a pair with {@link DirMeta}.
+ *
  * <p>Referenced by {@link Commit#rootDirTreeChecksum()} and recursively by {@link
  * Directory#treeChecksum()}.
  *
  * <p>Reference: {@code ostree-core.h#OSTREE_TREE_GVARIANT_STRING}
+ *
+ * @param files a list of files in the directory.
+ * @param directories a list of subdirectories of the directory.
+ * @see DirMeta
+ * @see ObjectType#DIR_TREE
  */
 public record DirTree(List<File> files, List<Directory> directories) {
 
+  /**
+   * A file in a file tree.
+   *
+   * @param name the file name.
+   * @param checksum the checksum of the {@link ObjectType#FILE} object.
+   */
   public record File(String name, Checksum checksum) {
 
     private static final Decoder<File> DECODER =
         Decoder.ofStructure(
             File.class, Decoder.ofString(StandardCharsets.UTF_8), Checksum.decoder());
 
+    /**
+     * Acquires a {@link Decoder} for the enclosing type.
+     *
+     * @return a possibly shared {@link Decoder}.
+     */
     public static Decoder<File> decoder() {
       return DECODER;
     }
   }
 
+  /**
+   * A subdirectory in a file tree.
+   *
+   * @param name the name of the subdirectory.
+   * @param treeChecksum the checksum of the {@link DirTree} object.
+   * @param dirChecksum the checksum of the {@link DirMeta} object.
+   */
   public record Directory(String name, Checksum treeChecksum, Checksum dirChecksum) {
 
     private static final Decoder<Directory> DECODER =
@@ -34,6 +59,11 @@
             Checksum.decoder(),
             Checksum.decoder());
 
+    /**
+     * Acquires a {@link Decoder} for the enclosing type.
+     *
+     * @return a possibly shared {@link Decoder}.
+     */
     public static Decoder<Directory> decoder() {
       return DECODER;
     }
@@ -43,6 +73,11 @@
       Decoder.ofStructure(
           DirTree.class, Decoder.ofArray(File.decoder()), Decoder.ofArray(Directory.decoder()));
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<DirTree> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/FileMeta.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/FileMeta.java
index 19e0677..2786ce9 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/FileMeta.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/FileMeta.java
@@ -7,7 +7,15 @@
 /**
  * Permission bits and extended attributes for a file.
  *
+ * <p>Stored in a POSIX extended attribute on the corresponding {@link ObjectType#FILE} object in
+ * repositories in “bare-user” format.
+ *
  * <p>Reference: {@code ostree-core.h#OSTREE_FILEMETA_GVARIANT_STRING}
+ *
+ * @param uid the user ID that owns the file.
+ * @param gid the group ID that owns the file.
+ * @param mode the POSIX permission bits.
+ * @param xattrs POSIX extended attributes.
  */
 public record FileMeta(int uid, int gid, int mode, List<Xattr> xattrs) {
 
@@ -19,6 +27,11 @@
           Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
           Decoder.ofArray(Xattr.decoder()));
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<FileMeta> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Metadata.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Metadata.java
index 8bb5255..fd8df28 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Metadata.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Metadata.java
@@ -6,9 +6,11 @@
 import java.util.Map;
 
 /**
- * A wrapper for a list of metadata fields.
+ * A wrapper for a set of metadata fields.
  *
  * <p>Reference: (embedded in other data types)
+ *
+ * @param fields a set of metadata fields indexed by name.
  */
 public record Metadata(Map<String, Variant> fields) {
 
@@ -16,6 +18,11 @@
       Decoder.ofDictionary(Decoder.ofString(StandardCharsets.UTF_8), Decoder.ofVariant())
           .map(Metadata::new);
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<Metadata> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ObjectType.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ObjectType.java
new file mode 100644
index 0000000..28371fc
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ObjectType.java
@@ -0,0 +1,80 @@
+package eu.mulk.jgvariant.ostree;
+
+import static org.apiguardian.api.API.Status.STABLE;
+
+import org.apiguardian.api.API;
+
+/**
+ * An object type as found in an OSTree repository.
+ *
+ * <p>Each object type has its own file extension.
+ *
+ * <p>In an OSTree repository, objects are located in a subfolder of the {@code /objects} folder
+ * based on their {@link Checksum}. The schema for looking up objects is {@code
+ * /objects/{checksumHead}/{checksumRest}.{fileExtension}} where:
+ *
+ * <dl>
+ *   <dt>{@code {checksumHead}}
+ *   <dd>the first two characters of {@link Checksum#hex()}
+ *   <dt>{@code {checksumRest}}
+ *   <dd>the substring of {@link Checksum#hex()} starting from the 3rd character
+ *   <dt>{@code {fileExtension}}
+ *   <dd>the {@link #fileExtension()} of the object type
+ * </dl>
+ */
+@API(status = STABLE)
+public enum ObjectType {
+  FILE((byte) 1, "file"),
+  DIR_TREE((byte) 2, "dirtree"),
+  DIR_META((byte) 3, "dirmeta"),
+  COMMIT((byte) 4, "commit"),
+  TOMBSTONE_COMMIT((byte) 5, "commit-tombstone"),
+  COMMIT_META((byte) 6, "commitmeta"),
+  PAYLOAD_LINK((byte) 7, "payload-link");
+
+  private final byte byteValue;
+  private final String fileExtension;
+
+  /**
+   * The serialized byte value.
+   *
+   * @return a byte representing this value in serialized GVariant structures.
+   */
+  public byte byteValue() {
+    return byteValue;
+  }
+
+  /**
+   * The file extension carried by files of this type.
+   *
+   * @return a file extension.
+   */
+  public String fileExtension() {
+    return fileExtension;
+  }
+
+  ObjectType(byte byteValue, String fileExtension) {
+    this.byteValue = byteValue;
+    this.fileExtension = fileExtension;
+  }
+
+  /**
+   * Returns the {@link ObjectType} corresponding to a serialized GVariant value.
+   *
+   * @param byteValue a serialized value as used in GVariant.
+   * @return the {@link ObjectType} corresponding to the serialized value.
+   * @throws IllegalArgumentException if the byte value is invalid.
+   */
+  public static ObjectType valueOf(byte byteValue) {
+    return switch (byteValue) {
+      case 1 -> FILE;
+      case 2 -> DIR_TREE;
+      case 3 -> DIR_META;
+      case 4 -> COMMIT;
+      case 5 -> TOMBSTONE_COMMIT;
+      case 6 -> COMMIT_META;
+      case 7 -> PAYLOAD_LINK;
+      default -> throw new IllegalArgumentException("invalid ObjectType: %d".formatted(byteValue));
+    };
+  }
+}
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SignedDelta.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SignedDelta.java
index 303e344..0077cb4 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SignedDelta.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SignedDelta.java
@@ -2,6 +2,7 @@
 
 import eu.mulk.jgvariant.core.Decoder;
 import eu.mulk.jgvariant.core.Variant;
+import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
@@ -10,17 +11,33 @@
  * A {@link DeltaSuperblock} signed with some sort of key.
  *
  * <p>Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_SIGNED_FORMAT}
+ *
+ * @param magicNumber the value {@link #MAGIC}.
+ * @param superblock the {@link DeltaSuperblock}.
+ * @param signatures a list of signatures, indexed by type.
  */
 public record SignedDelta(
-    long magicNumber, ByteString superblock, Map<String, Variant> signatures) {
+    long magicNumber, DeltaSuperblock superblock, Map<String, Variant> signatures) {
+
+  /** The value of {@link #magicNumber()}. */
+  public static final long MAGIC = 0x4F53_5453_474E_4454L;
 
   private static final Decoder<SignedDelta> DECODER =
       Decoder.ofStructure(
           SignedDelta.class,
           Decoder.ofLong().withByteOrder(ByteOrder.BIG_ENDIAN),
-          ByteString.decoder(),
+          ByteString.decoder().map(SignedDelta::decodeSuperblock),
           Decoder.ofDictionary(Decoder.ofString(StandardCharsets.US_ASCII), Decoder.ofVariant()));
 
+  private static DeltaSuperblock decodeSuperblock(ByteString byteString) {
+    return DeltaSuperblock.decoder().decode(ByteBuffer.wrap(byteString.bytes()));
+  }
+
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<SignedDelta> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Summary.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Summary.java
index 8f4ddf6..bc94436 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Summary.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Summary.java
@@ -11,11 +11,29 @@
  * <p>Stored as a file named {@code summary} in the OSTree repository root.
  *
  * <p>Reference: {@code ostree-core.h#OSTREE_SUMMARY_GVARIANT_STRING}
+ *
+ * @param entries an entry in the summary file.
+ * @param metadata additional keys and values contained in the summary.
  */
 public record Summary(List<Entry> entries, Metadata metadata) {
 
+  /**
+   * An entry in the summary file of an OSTree repository, describing a ref.
+   *
+   * @param ref a ref name.
+   * @param value data describing the ref.
+   */
   public record Entry(String ref, Value value) {
 
+    /**
+     * The value part of an entry in the summary file of an OSTree repository.
+     *
+     * <p>Describes the {@link Commit} currently named by the corresponding ref.
+     *
+     * @param size the size of the commit.
+     * @param checksum the checksum of the commit.
+     * @param metadata additional metadata describing the commit.
+     */
     public record Value(long size, Checksum checksum, Metadata metadata) {
 
       private static final Decoder<Value> DECODER =
@@ -25,6 +43,11 @@
               Checksum.decoder(),
               Metadata.decoder());
 
+      /**
+       * Acquires a {@link Decoder} for the enclosing type.
+       *
+       * @return a possibly shared {@link Decoder}.
+       */
       public static Decoder<Value> decoder() {
         return DECODER;
       }
@@ -33,6 +56,11 @@
     private static final Decoder<Entry> DECODER =
         Decoder.ofStructure(Entry.class, Decoder.ofString(StandardCharsets.UTF_8), Value.decoder());
 
+    /**
+     * Acquires a {@link Decoder} for the enclosing type.
+     *
+     * @return a possibly shared {@link Decoder}.
+     */
     public static Decoder<Entry> decoder() {
       return DECODER;
     }
@@ -41,6 +69,11 @@
   private static final Decoder<Summary> DECODER =
       Decoder.ofStructure(Summary.class, Decoder.ofArray(Entry.decoder()), Metadata.decoder());
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<Summary> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SummarySignature.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SummarySignature.java
index 13bb432..efa48c3 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SummarySignature.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SummarySignature.java
@@ -11,6 +11,8 @@
  * <p>Stored as a file named {@code summary.sig} in the OSTree repository root.
  *
  * <p>Reference: {@code ostree-repo-static-delta-private.h#OSTREE_SUMMARY_SIG_GVARIANT_STRING}
+ *
+ * @param signatures a list of signatures, indexed by type.
  */
 public record SummarySignature(Map<String, Variant> signatures) {
 
@@ -18,6 +20,11 @@
       Decoder.ofDictionary(Decoder.ofString(StandardCharsets.UTF_8), Decoder.ofVariant())
           .map(SummarySignature::new);
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<SummarySignature> decoder() {
     return DECODER;
   }
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Xattr.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Xattr.java
index 68628c4..7f05cf6 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Xattr.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Xattr.java
@@ -3,15 +3,27 @@
 import eu.mulk.jgvariant.core.Decoder;
 
 /**
- * Reference: (embedded in other data types, e.g. {@code
+ * A POSIX extended attribute of a file or directory.
+ *
+ * <p>Reference: (embedded in other data types, e.g. {@code
  * ostree-core.h#OSTREE_DIRMETA_GVARIANT_STRING}, {@code
  * ostree-core.h#OSTREE_FILEMETA_GVARIANT_STRING})
+ *
+ * @param name the name part of the extended attribute.
+ * @param value the value part of the extended attribute.
+ * @see DirMeta
+ * @see FileMeta
  */
 public record Xattr(ByteString name, ByteString value) {
 
   private static final Decoder<Xattr> DECODER =
       Decoder.ofStructure(Xattr.class, ByteString.decoder(), ByteString.decoder());
 
+  /**
+   * Acquires a {@link Decoder} for the enclosing type.
+   *
+   * @return a possibly shared {@link Decoder}.
+   */
   public static Decoder<Xattr> decoder() {
     return DECODER;
   }