Document repository layout, add modified Base64 ByteString encoding.

Change-Id: I564db0e346346b608fa11527590e264c694fedaf
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 cf6e99a..bb8cbb6 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
@@ -2,6 +2,7 @@
 
 import eu.mulk.jgvariant.core.Decoder;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.HexFormat;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
@@ -62,6 +63,34 @@
   }
 
   /**
+   * Converts the contained byte array into modified Base64 (with {@code "/"} replaced with {@code
+   * "-"}).
+   *
+   * <p>Modified Base64 is Base64 with {@code "/"} replaced with {@code "_"}. It is used to address
+   * static deltas in an OSTree repository.
+   *
+   * <p>Useful for printing.
+   *
+   * @return a modified Base64 representation of the bytes making up this checksum.
+   */
+  public String modifiedBase64() {
+    return Base64.getEncoder().withoutPadding().encodeToString(bytes).replace('/', '_');
+  }
+
+  /**
+   * Parses a modified Base64 string into a {@link Checksum}.
+   *
+   * <p>Modified Base64 is Base64 with {@code "/"} replaced with {@code "_"}. It is used to address
+   * static deltas in an OSTree repository.
+   *
+   * @param mbase64 a hex string.
+   * @return a {@link Checksum} corresponding to the given modified Base64 string.
+   */
+  public static ByteString ofModifiedBase64(String mbase64) {
+    return new ByteString(Base64.getDecoder().decode(mbase64.replace('_', '/')));
+  }
+
+  /**
    * Returns the number of bytes in the byte string.
    *
    * @return the number of bytes in the byte string.
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 ce5f9b0..94a728c 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
@@ -76,6 +76,34 @@
   }
 
   /**
+   * Converts the contained byte array into modified Base64 (with {@code "/"} replaced with {@code
+   * "-"}).
+   *
+   * <p>Modified Base64 is Base64 with {@code "/"} replaced with {@code "_"}. It is used to address
+   * static deltas in an OSTree repository.
+   *
+   * <p>Useful for printing.
+   *
+   * @return a modified Base64 representation of the bytes making up this checksum.
+   */
+  public String modifiedBase64() {
+    return byteString.modifiedBase64();
+  }
+
+  /**
+   * Parses a modified Base64 string into a {@link Checksum}.
+   *
+   * <p>Modified Base64 is Base64 with {@code "/"} replaced with {@code "_"}. It is used to address
+   * static deltas in an OSTree repository.
+   *
+   * @param mbase64 a hex string.
+   * @return a {@link Checksum} corresponding to the given modified Base64 string.
+   */
+  public static Checksum ofModifiedBase64(String mbase64) {
+    return new Checksum(ByteString.ofModifiedBase64(mbase64));
+  }
+
+  /**
    * Reads a Checksum for a {@link ByteBuffer}.
    *
    * @param byteBuffer the byte buffer to read from.
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
index 28371fc..721d857 100644
--- a/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ObjectType.java
+++ b/jgvariant-ostree/src/main/java/eu/mulk/jgvariant/ostree/ObjectType.java
@@ -24,12 +24,56 @@
  */
 @API(status = STABLE)
 public enum ObjectType {
+
+  /**
+   * A regular file.
+   *
+   * <p>File ending: {@code .file}
+   */
   FILE((byte) 1, "file"),
+
+  /**
+   * A serialized {@link DirTree} object.
+   *
+   * <p>File ending: {@code .dirtree}
+   */
   DIR_TREE((byte) 2, "dirtree"),
+
+  /**
+   * A serialized {@link DirMeta} object.
+   *
+   * <p>File ending: {@code .dirmeta}
+   */
   DIR_META((byte) 3, "dirmeta"),
+
+  /**
+   * A serialized {@link Commit} object.
+   *
+   * <p>File ending: {@code .commit}
+   */
   COMMIT((byte) 4, "commit"),
+
+  /**
+   * A tombstone file standing in for a commit that was deleted.
+   *
+   * <p>File ending: {@code .commit-tombstone}
+   */
   TOMBSTONE_COMMIT((byte) 5, "commit-tombstone"),
+
+  /**
+   * Detached metadata for a {@link Commit}.
+   *
+   * <p>Often goes together with a {@link #TOMBSTONE_COMMIT}.
+   *
+   * <p>File ending: {@code .commitmeta}
+   */
   COMMIT_META((byte) 6, "commitmeta"),
+
+  /**
+   * A symlink to a {@link #FILE} that lives somewhere else.
+   *
+   * <p>File ending: {@code .payload-link}
+   */
   PAYLOAD_LINK((byte) 7, "payload-link");
 
   private final byte byteValue;
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
index c6a61f1..028f4cd 100644
--- 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
@@ -2,6 +2,183 @@
  * 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.
+ *
+ * <h2>OStree repository structure</h2>
+ *
+ * <p>An OSTree repository has the following layout:
+ *
+ * <table>
+ *   <caption>OSTree repository layout</caption>
+ *
+ *   <tr>
+ *     <td>{@code config}</td>
+ *     <td>
+ *       <p>
+ *         A plain text file that contains various settings.  Among other things, this defines
+ *         the <a href="https://ostreedev.github.io/ostree/formats/#the-archive-format">archive
+ *         format</a> of the repository and whether files are compressed ({@code mode=archive-z2})
+ *         or plain ({@code mode=bare}, {@code mode=bare-user}).
+ *       </p>
+ *     </td>
+ *   </tr>
+ *
+ *   <tr>
+ *     <td>{@code summary}</td>
+ *     <td>
+ *       <p>
+ *         A {@link eu.mulk.jgvariant.ostree.Summary} object containing information on what is
+ *         available under {@code refs/}, {@code deltas/}, and {@code delta-indexes/}.
+ *       </p>
+ *
+ *       <p>This file may or may not exist and, if it exists, may or may not be up to date.</p>
+ *     </td>
+ *   </tr>
+ *
+ *   <tr>
+ *     <td>{@code refs/heads/{name...}}</td>
+ *     <td>Plain-text files containing {@link eu.mulk.jgvariant.ostree.Checksum}s in hex format
+ *     (see {@link eu.mulk.jgvariant.ostree.Checksum#ofHex}) referring to
+ *     {@link eu.mulk.jgvariant.ostree.Commit} objects.  See below for the layout of the
+ *     {@code objects/} directory that this refers to.</td>
+ *   </tr>
+ *
+ *   <tr>
+ *     <td>{@code objects/{checksumHead}/{checksumRest}.{fileExtension}}</td>
+ *     <td>
+ *       <p>
+ *         Objects of various types as described by {@link eu.mulk.jgvariant.ostree.ObjectType}
+ *         and keyed by {@link eu.mulk.jgvariant.ostree.Checksum}.
+ *       </p>
+ *
+ *       <p>
+ *         Among other things, this is where you find {@link eu.mulk.jgvariant.ostree.Commit}
+ *         ({@code .commit)}, {@link eu.mulk.jgvariant.ostree.DirTree} ({@code .dirtree}),
+ *         and {@link eu.mulk.jgvariant.ostree.DirMeta} ({@code .dirmeta}) objects as well as
+ *         plain ({@code .file}) or compressed files ({@code .filez}).
+ *       </p>
+ *
+ *       <p>
+ *         Static delta information is not stored here, but in the {@code deltas/} and
+ *         {@code delta-indexes/} directories (if available).
+ *       </p>
+ *
+ *       <p>
+ *         The individual parts of the file path are defined as follows:
+ *       </p>
+ *
+ *       <dl>
+ *         <dt>{@code {checksumHead}}
+ *         <dd>
+ *           the first two characters of {@link eu.mulk.jgvariant.ostree.Checksum#hex()}
+ *         <dt>{@code {checksumRest}}
+ *         <dd>
+ *           the substring of {@link eu.mulk.jgvariant.ostree.Checksum#hex()} starting from the
+ *           3rd character
+ *         <dt>{@code {fileExtension}}
+ *         <dd>
+ *           the {@link eu.mulk.jgvariant.ostree.ObjectType#fileExtension()} of the object type
+ *       </dl>
+ *     </td>
+ *   </tr>
+ *
+ *   <tr id="delta-superblock">
+ *     <td>{@code deltas/{targetChecksumMbase64Head}/{targetChecksumMbase64Rest}/superblock}</td>
+ *     <td>
+ *       <p>
+ *         A {@link eu.mulk.jgvariant.ostree.DeltaSuperblock} to get from nothing (an empty commit)
+ *         to the commit named by the checksum encoded in the path.
+ *       </p>
+ *
+ *       <p>
+ *         The individual parts of the file path are defined as follows:
+ *       </p>
+ *
+ *       <dl>
+ *         <dt>{@code {checksumHead}}
+ *         <dd>
+ *           the first two characters of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()}
+ *         <dt>{@code {checksumRest}}
+ *         <dd>
+ *           the substring of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()} starting
+ *           from the 3rd character
+ *       </dl>
+ *     </td>
+ *   </tr>
+ *
+ *   <tr>
+ *     <td>{@code deltas/{targetChecksumMbase64Head}/{targetChecksumMbase64Rest}/{digit...}}</td>
+ *     <td>
+ *       <p>
+ *         A {@link eu.mulk.jgvariant.ostree.DeltaPartPayload} belonging to a delta that goes from
+ *         nothing (an empty commit) to the commit named by the checksum encoded in the path.
+ *       </p>
+ *
+ *       <p>
+ *         The individual parts of the file path are defined as follows:
+ *       </p>
+ *
+ *       <dl>
+ *         <dt>{@code {checksumHead}}
+ *         <dd>
+ *           the first two characters of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()}
+ *         <dt>{@code {checksumRest}}
+ *         <dd>
+ *           the substring of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()} starting
+ *           from the 3rd character
+ *       </dl>
+ *     </td>
+ *   </tr>
+ *
+ *   <tr>
+ *     <td>{@code deltas/{sourceChecksumMbase64Head}/{sourceChecksumMbase64Rest}-{targetChecksumMbase64}/superblock}</td>
+ *     <td>
+ *       <p>
+ *         A {@link eu.mulk.jgvariant.ostree.DeltaSuperblock} to get from the source commit
+ *         referenced by the first checksum encoded in the path to the target commit referenced
+ *         in the second checksum encoded in the path.
+ *       </p>
+ *
+ *       <p>
+ *         The individual parts of the file path are defined as follows:
+ *       </p>
+ *
+ *       <dl>
+ *         <dt>{@code {checksumHead}}
+ *         <dd>
+ *           the first two characters of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()}
+ *         <dt>{@code {checksumRest}}
+ *         <dd>
+ *           the substring of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()} starting
+ *           from the 3rd character
+ *       </dl>
+ *     </td>
+ *   </tr>
+ *
+ *   <tr>
+ *     <td>{@code deltas/{sourceChecksumMbase64Head}/{sourceChecksumMbase64Rest}-{targetChecksumMbase64}/{digit...}}</td>
+ *     <td>
+ *       <p>
+ *         A {@link eu.mulk.jgvariant.ostree.DeltaPartPayload} belonging to a delta that goes from
+ *         the source commit referenced by the first checksum encoded in the path to the target
+ *         commit referenced in the second checksum encoded in the path.
+ *       </p>
+ *
+ *       <p>
+ *         The individual parts of the file path are defined as follows:
+ *       </p>
+ *
+ *       <dl>
+ *         <dt>{@code {checksumHead}}
+ *         <dd>
+ *           the first two characters of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()}
+ *         <dt>{@code {checksumRest}}
+ *         <dd>
+ *           the substring of {@link eu.mulk.jgvariant.ostree.Checksum#modifiedBase64()} starting
+ *           from the 3rd character
+ *       </dl>
+ *     </td>
+ *   </tr>
+ * </table>
  */
 @API(status = Status.EXPERIMENTAL)
 package eu.mulk.jgvariant.ostree;
diff --git a/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/ByteStringTest.java b/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/ByteStringTest.java
new file mode 100644
index 0000000..a5fa2b4
--- /dev/null
+++ b/jgvariant-ostree/src/test/java/eu/mulk/jgvariant/ostree/ByteStringTest.java
@@ -0,0 +1,70 @@
+package eu.mulk.jgvariant.ostree;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class ByteStringTest {
+
+  @Test
+  void testToModifiedBase64() {
+    assertEquals("MciDXVydLGaHpQCRyFFC0bLYU_9Bap+4G07jB1RRDVI", testByteString1.modifiedBase64());
+  }
+
+  @Test
+  void testOfModifiedBase64() {
+    assertEquals(
+        testByteString1,
+        ByteString.ofModifiedBase64("MciDXVydLGaHpQCRyFFC0bLYU_9Bap+4G07jB1RRDVI"));
+  }
+
+  @Test
+  void testToHex() {
+    assertEquals(
+        "31c8835d5c9d2c6687a50091c85142d1b2d853ff416a9fb81b4ee30754510d52", testByteString1.hex());
+  }
+
+  @Test
+  void testOfHex() {
+    assertEquals(
+        testByteString1,
+        ByteString.ofHex("31c8835d5c9d2c6687a50091c85142d1b2d853ff416a9fb81b4ee30754510d52"));
+  }
+
+  private static final ByteString testByteString1 =
+      new ByteString(
+          new byte[] {
+            (byte) 0x31,
+            (byte) 0xc8,
+            (byte) 0x83,
+            (byte) 0x5d,
+            (byte) 0x5c,
+            (byte) 0x9d,
+            (byte) 0x2c,
+            (byte) 0x66,
+            (byte) 0x87,
+            (byte) 0xa5,
+            (byte) 0x00,
+            (byte) 0x91,
+            (byte) 0xc8,
+            (byte) 0x51,
+            (byte) 0x42,
+            (byte) 0xd1,
+            (byte) 0xb2,
+            (byte) 0xd8,
+            (byte) 0x53,
+            (byte) 0xff,
+            (byte) 0x41,
+            (byte) 0x6a,
+            (byte) 0x9f,
+            (byte) 0xb8,
+            (byte) 0x1b,
+            (byte) 0x4e,
+            (byte) 0xe3,
+            (byte) 0x07,
+            (byte) 0x54,
+            (byte) 0x51,
+            (byte) 0x0d,
+            (byte) 0x52
+          });
+}
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 db222f9..031b1cd 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
@@ -2,7 +2,6 @@
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.adelean.inject.resources.junit.jupiter.GivenBinaryResource;
 import com.adelean.inject.resources.junit.jupiter.TestWithResources;
@@ -38,11 +37,6 @@
   byte[] deltaPartPayloadBytes;
 
   @Test
-  void testTrivial() {
-    assertTrue(true);
-  }
-
-  @Test
   void testSummaryDecoder() {
     var decoder = Summary.decoder();
     var summary = decoder.decode(ByteBuffer.wrap(summaryBytes));