aboutsummaryrefslogtreecommitdiff
path: root/jgvariant-ostree/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'jgvariant-ostree/src/main/java')
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ByteString.java41
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Checksum.java24
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Commit.java51
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaFallback.java25
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaMetaEntry.java38
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaPartPayload.java41
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaSuperblock.java43
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirMeta.java25
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirTree.java49
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/FileMeta.java25
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Metadata.java33
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SignedDelta.java38
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Summary.java47
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SummarySignature.java35
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Xattr.java18
-rw-r--r--jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/package-info.java10
-rw-r--r--jgvariant-ostree/src/main/java/module-info.java85
17 files changed, 628 insertions, 0 deletions
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
new file mode 100644
index 0000000..1a85547
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ByteString.java
@@ -0,0 +1,41 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.util.Arrays;
+import java.util.HexFormat;
+
+/**
+ * A wrapper for a {@code byte[]} that implements {@link #equals(Object)}, {@link #hashCode()}, and
+ * {@link #toString()} according to value semantics.
+ */
+public record ByteString(byte[] bytes) {
+
+ private static final Decoder<ByteString> DECODER = Decoder.ofByteArray().map(ByteString::new);
+
+ public static Decoder<ByteString> decoder() {
+ return DECODER;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof ByteString byteString) && Arrays.equals(bytes, byteString.bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(bytes);
+ }
+
+ @Override
+ public String toString() {
+ return "ByteString{hex=\"%s\"}".formatted(hex());
+ }
+
+ public String hex() {
+ return HexFormat.of().formatHex(bytes);
+ }
+
+ public static ByteString ofHex(String hex) {
+ return new ByteString(HexFormat.of().parseHex(hex));
+ }
+}
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
new file mode 100644
index 0000000..f77eb57
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Checksum.java
@@ -0,0 +1,24 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+
+/**
+ * A wrapper for {@link ByteString} that refers to a content-addressed object in an OSTree
+ * repository.
+ */
+public record Checksum(ByteString bytes) {
+
+ private static final Decoder<Checksum> DECODER = ByteString.decoder().map(Checksum::new);
+
+ public static Decoder<Checksum> decoder() {
+ return DECODER;
+ }
+
+ public String hex() {
+ return bytes.hex();
+ }
+
+ public static Checksum ofHex(String hex) {
+ return new Checksum(ByteString.ofHex(hex));
+ }
+}
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
new file mode 100644
index 0000000..43909ba
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Commit.java
@@ -0,0 +1,51 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * A commit in an OSTree repository.
+ *
+ * <p>Has an optional parent, a root directory, and various metadata.
+ *
+ * <p>Reference: {@code ostree-core.h#OSTREE_COMMIT_GVARIANT_STRING}
+ */
+public record Commit(
+ Metadata metadata,
+ Checksum parentChecksum,
+ List<RelatedObject> relatedObjects,
+ String subject,
+ String body,
+ long timestamp,
+ Checksum rootDirTreeChecksum,
+ Checksum rootDirMetaChecksum) {
+
+ public record RelatedObject(String ref, Checksum commitChecksum) {
+
+ private static final Decoder<RelatedObject> DECODER =
+ Decoder.ofStructure(
+ RelatedObject.class, Decoder.ofString(StandardCharsets.UTF_8), Checksum.decoder());
+
+ public static Decoder<RelatedObject> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<Commit> DECODER =
+ Decoder.ofStructure(
+ Commit.class,
+ Metadata.decoder(),
+ Checksum.decoder(),
+ Decoder.ofArray(RelatedObject.decoder()),
+ Decoder.ofString(StandardCharsets.UTF_8),
+ Decoder.ofString(StandardCharsets.UTF_8),
+ Decoder.ofLong().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Checksum.decoder(),
+ Checksum.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
new file mode 100644
index 0000000..1b3cc0a
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaFallback.java
@@ -0,0 +1,25 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+
+/**
+ * A fallback entry in a {@link DeltaSuperblock}.
+ *
+ * <p>Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_FALLBACK_FORMAT}
+ */
+public record DeltaFallback(
+ byte objectType, Checksum checksum, long compressedSize, long uncompressedSize) {
+
+ private static final Decoder<DeltaFallback> DECODER =
+ Decoder.ofStructure(
+ DeltaFallback.class,
+ Decoder.ofByte(),
+ Checksum.decoder(),
+ Decoder.ofLong().withByteOrder(ByteOrder.LITTLE_ENDIAN), // FIXME: non-canonical
+ Decoder.ofLong().withByteOrder(ByteOrder.LITTLE_ENDIAN)); // FIXME: non-canonical
+
+ 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
new file mode 100644
index 0000000..39ada86
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaMetaEntry.java
@@ -0,0 +1,38 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+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}
+ */
+public record DeltaMetaEntry(
+ int version, Checksum checksum, long size, long usize, List<DeltaObject> objects) {
+
+ record DeltaObject(byte objectType, Checksum checksum) {
+
+ private static final Decoder<DeltaObject> DECODER =
+ Decoder.ofStructure(DeltaObject.class, Decoder.ofByte(), Checksum.decoder());
+
+ public static Decoder<DeltaObject> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<DeltaMetaEntry> DECODER =
+ Decoder.ofStructure(
+ DeltaMetaEntry.class,
+ Decoder.ofInt().withByteOrder(ByteOrder.LITTLE_ENDIAN), // FIXME: non-canonical
+ 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
+ );
+
+ public static Decoder<DeltaMetaEntry> decoder() {
+ return DECODER;
+ }
+}
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
new file mode 100644
index 0000000..c8c5fe7
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaPartPayload.java
@@ -0,0 +1,41 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+import java.util.List;
+
+/**
+ * Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_PART_PAYLOAD_FORMAT_V0}
+ */
+public record DeltaPartPayload(
+ List<FileMode> fileModes,
+ List<List<Xattr>> xattrs,
+ ByteString rawDataSource,
+ ByteString 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());
+
+ 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));
+
+ public static Decoder<FileMode> decoder() {
+ return 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
new file mode 100644
index 0000000..2b09645
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DeltaSuperblock.java
@@ -0,0 +1,43 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+import java.util.List;
+
+/** Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_SUPERBLOCK_FORMAT} */
+public record DeltaSuperblock(
+ Metadata metadata,
+ long timestamp,
+ Checksum fromChecksum,
+ Checksum toChecksum,
+ Commit commit,
+ List<DeltaName> dependencies,
+ List<DeltaMetaEntry> entries,
+ List<DeltaFallback> fallbacks) {
+
+ public record DeltaName(Checksum fromChecksum, Checksum toChecksum) {
+
+ private static final Decoder<DeltaName> DECODER =
+ Decoder.ofStructure(DeltaName.class, Checksum.decoder(), Checksum.decoder());
+
+ public static Decoder<DeltaName> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<DeltaSuperblock> DECODER =
+ Decoder.ofStructure(
+ DeltaSuperblock.class,
+ Metadata.decoder(),
+ Decoder.ofLong().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Checksum.decoder(),
+ Checksum.decoder(),
+ Commit.decoder(),
+ Decoder.ofByteArray().map(x -> List.of()), // FIXME
+ Decoder.ofArray(DeltaMetaEntry.decoder()),
+ Decoder.ofArray(DeltaFallback.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
new file mode 100644
index 0000000..3a9672d
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirMeta.java
@@ -0,0 +1,25 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+import java.util.List;
+
+/**
+ * Permission bits and extended attributes for a directory.
+ *
+ * <p>Reference: {@code ostree-core.h#OSTREE_DIRMETA_GVARIANT_STRING}
+ */
+public record DirMeta(int uid, int gid, int mode, List<Xattr> xattrs) {
+
+ private static final Decoder<DirMeta> DECODER =
+ Decoder.ofStructure(
+ DirMeta.class,
+ Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Decoder.ofArray(Xattr.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
new file mode 100644
index 0000000..3a14abb
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/DirTree.java
@@ -0,0 +1,49 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Metadata describing files and directories of a file tree.
+ *
+ * <p>Referenced by {@link Commit#rootDirTreeChecksum()} and recursively by {@link
+ * Directory#treeChecksum()}.
+ *
+ * <p>Reference: {@code ostree-core.h#OSTREE_TREE_GVARIANT_STRING}
+ */
+public record DirTree(List<File> files, List<Directory> directories) {
+
+ public record File(String name, Checksum checksum) {
+
+ private static final Decoder<File> DECODER =
+ Decoder.ofStructure(
+ File.class, Decoder.ofString(StandardCharsets.UTF_8), Checksum.decoder());
+
+ public static Decoder<File> decoder() {
+ return DECODER;
+ }
+ }
+
+ public record Directory(String name, Checksum treeChecksum, Checksum dirChecksum) {
+
+ private static final Decoder<Directory> DECODER =
+ Decoder.ofStructure(
+ Directory.class,
+ Decoder.ofString(StandardCharsets.UTF_8),
+ Checksum.decoder(),
+ Checksum.decoder());
+
+ public static Decoder<Directory> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<DirTree> DECODER =
+ Decoder.ofStructure(
+ DirTree.class, Decoder.ofArray(File.decoder()), Decoder.ofArray(Directory.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
new file mode 100644
index 0000000..19e0677
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/FileMeta.java
@@ -0,0 +1,25 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+import java.util.List;
+
+/**
+ * Permission bits and extended attributes for a file.
+ *
+ * <p>Reference: {@code ostree-core.h#OSTREE_FILEMETA_GVARIANT_STRING}
+ */
+public record FileMeta(int uid, int gid, int mode, List<Xattr> xattrs) {
+
+ private static final Decoder<FileMeta> DECODER =
+ Decoder.ofStructure(
+ FileMeta.class,
+ Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
+ Decoder.ofArray(Xattr.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
new file mode 100644
index 0000000..cf838d3
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Metadata.java
@@ -0,0 +1,33 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import eu.mulk.jgvariant.core.Variant;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * A wrapper for a list of metadata fields.
+ *
+ * <p>Reference: (embedded in other data types)
+ */
+public record Metadata(List<Field> fields) {
+
+ /** A metadata field with a key and a value. */
+ public record Field(String key, Variant value) {
+
+ private static final Decoder<Field> DECODER =
+ Decoder.ofStructure(
+ Field.class, Decoder.ofString(StandardCharsets.UTF_8), Decoder.ofVariant());
+
+ public static Decoder<Field> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<Metadata> DECODER =
+ Decoder.ofArray(Field.decoder()).map(Metadata::new);
+
+ public static Decoder<Metadata> decoder() {
+ return DECODER;
+ }
+}
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
new file mode 100644
index 0000000..2fc5c25
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SignedDelta.java
@@ -0,0 +1,38 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import eu.mulk.jgvariant.core.Variant;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * A {@link DeltaSuperblock} signed with some sort of key.
+ *
+ * <p>Reference: {@code ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_SIGNED_FORMAT}
+ */
+public record SignedDelta(
+ long magicNumber, ByteString superblock, List<SignedDelta.Signature> signatures) {
+
+ /** A cryptographic signature. */
+ public record Signature(String key, Variant data) {
+ private static final Decoder<Signature> DECODER =
+ Decoder.ofStructure(
+ Signature.class, Decoder.ofString(StandardCharsets.US_ASCII), Decoder.ofVariant());
+
+ public static Decoder<Signature> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<SignedDelta> DECODER =
+ Decoder.ofStructure(
+ SignedDelta.class,
+ Decoder.ofLong().withByteOrder(ByteOrder.BIG_ENDIAN),
+ ByteString.decoder(),
+ Decoder.ofArray(Signature.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
new file mode 100644
index 0000000..8f4ddf6
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Summary.java
@@ -0,0 +1,47 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * The summary file of an OSTree repository.
+ *
+ * <p>Stored as a file named {@code summary} in the OSTree repository root.
+ *
+ * <p>Reference: {@code ostree-core.h#OSTREE_SUMMARY_GVARIANT_STRING}
+ */
+public record Summary(List<Entry> entries, Metadata metadata) {
+
+ public record Entry(String ref, Value value) {
+
+ public record Value(long size, Checksum checksum, Metadata metadata) {
+
+ private static final Decoder<Value> DECODER =
+ Decoder.ofStructure(
+ Value.class,
+ Decoder.ofLong().withByteOrder(ByteOrder.LITTLE_ENDIAN),
+ Checksum.decoder(),
+ Metadata.decoder());
+
+ public static Decoder<Value> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<Entry> DECODER =
+ Decoder.ofStructure(Entry.class, Decoder.ofString(StandardCharsets.UTF_8), Value.decoder());
+
+ public static Decoder<Entry> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<Summary> DECODER =
+ Decoder.ofStructure(Summary.class, Decoder.ofArray(Entry.decoder()), Metadata.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
new file mode 100644
index 0000000..a05b96d
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/SummarySignature.java
@@ -0,0 +1,35 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+import eu.mulk.jgvariant.core.Variant;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * A collection of cryptographic signatures for a {@link Summary}.
+ *
+ * <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}
+ */
+public record SummarySignature(List<Signature> signatures) {
+
+ /** A cryptographic signature. */
+ public record Signature(String key, Variant data) {
+
+ private static final Decoder<Signature> DECODER =
+ Decoder.ofStructure(
+ Signature.class, Decoder.ofString(StandardCharsets.UTF_8), Decoder.ofVariant());
+
+ public static Decoder<Signature> decoder() {
+ return DECODER;
+ }
+ }
+
+ private static final Decoder<SummarySignature> DECODER =
+ Decoder.ofArray(Signature.decoder()).map(SummarySignature::new);
+
+ 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
new file mode 100644
index 0000000..68628c4
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/Xattr.java
@@ -0,0 +1,18 @@
+package eu.mulk.jgvariant.ostree;
+
+import eu.mulk.jgvariant.core.Decoder;
+
+/**
+ * Reference: (embedded in other data types, e.g. {@code
+ * ostree-core.h#OSTREE_DIRMETA_GVARIANT_STRING}, {@code
+ * ostree-core.h#OSTREE_FILEMETA_GVARIANT_STRING})
+ */
+public record Xattr(ByteString name, ByteString value) {
+
+ private static final Decoder<Xattr> DECODER =
+ Decoder.ofStructure(Xattr.class, ByteString.decoder(), ByteString.decoder());
+
+ public static Decoder<Xattr> decoder() {
+ return DECODER;
+ }
+}
diff --git a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/package-info.java b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/package-info.java
new file mode 100644
index 0000000..c6a61f1
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Provides record classes describing the elements of <a
+ * href="https://ostreedev.github.io/ostree/">OSTree</a> repositories and factory methods to create
+ * {@link eu.mulk.jgvariant.core.Decoder} instances for them.
+ */
+@API(status = Status.EXPERIMENTAL)
+package eu.mulk.jgvariant.ostree;
+
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
diff --git a/jgvariant-ostree/src/main/java/module-info.java b/jgvariant-ostree/src/main/java/module-info.java
new file mode 100644
index 0000000..e51c586
--- /dev/null
+++ b/jgvariant-ostree/src/main/java/module-info.java
@@ -0,0 +1,85 @@
+/**
+ * Provides a parser for the <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a>
+ * serialization format.
+ *
+ * <ul>
+ * <li><a href="#sect-overview">Overview</a>
+ * <li><a href="#sect-installation">Installation</a>
+ * </ul>
+ *
+ * <h2 id="sect-overview">Overview</h2>
+ *
+ * <p>The {@link eu.mulk.jgvariant.ostree} package contains record classes describing the elements
+ * of <a href="https://ostreedev.github.io/ostree/">OSTree</a> repositories and factory methods to
+ * create {@link eu.mulk.jgvariant.core.Decoder} instances for them.
+ *
+ * <h2 id="sect-installation">Installation</h2>
+ *
+ * <ul>
+ * <li><a href="#sect-installation-maven">Usage with Maven</a>
+ * <li><a href="#sect-installation-gradle">Usage with Gradle</a>
+ * </ul>
+ *
+ * <h3 id="sect-installation-maven">Usage with Maven</h3>
+ *
+ * <pre>{@code
+ * <project>
+ * ...
+ *
+ * <dependencyManagement>
+ * ...
+ *
+ * <dependencies>
+ * <dependency>
+ * <groupId>eu.mulk.jgvariant</groupId>
+ * <artifactId>jgvariant-bom</artifactId>
+ * <version>0.1.4</version>
+ * <type>pom</type>
+ * <scope>import</scope>
+ * </dependency>
+ * </dependencies>
+ *
+ * ...
+ * </dependencyManagement>
+ *
+ * <dependencies>
+ * ...
+ *
+ * <dependency>
+ * <groupId>eu.mulk.jgvariant</groupId>
+ * <artifactId>jgvariant-core</artifactId>
+ * </dependency>
+ * <dependency>
+ * <groupId>eu.mulk.jgvariant</groupId>
+ * <artifactId>jgvariant-ostree</artifactId>
+ * </dependency>
+ *
+ * ...
+ * </dependencies>
+ *
+ * ...
+ * </project>
+ * }</pre>
+ *
+ * <h3 id="sect-installation-gradle">Usage with Gradle</h3>
+ *
+ * <pre>{@code
+ * dependencies {
+ * ...
+ *
+ * implementation(platform("eu.mulk.jgvariant:jgvariant-bom:0.1.4")
+ * implementation("eu.mulk.jgvariant:jgvariant-core")
+ * implementation("eu.mulk.jgvariant:jgvariant-ostree")
+ *
+ * ...
+ * }
+ * }</pre>
+ */
+module eu.mulk.jgvariant.gvariant {
+ requires transitive eu.mulk.jgvariant.core;
+ requires com.google.errorprone.annotations;
+ requires org.jetbrains.annotations;
+ requires org.apiguardian.api;
+
+ exports eu.mulk.jgvariant.ostree;
+}