jgvariant-tool: New module.

Adds a command line tool that can read and (in the future) manipulate
GVariant-formatted files.

Change-Id: Icc92eb409a97e7cf72dfd7535f6a8b3587dd4a48
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/Main.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/Main.java
new file mode 100644
index 0000000..64e375a
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/Main.java
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool;
+
+import static java.util.logging.Level.WARNING;
+
+import java.util.logging.Logger;
+import picocli.CommandLine;
+
+/**
+ * A command line tool to read and manipulate GVariant-formatted files.
+ *
+ * <p>Also provides ways to manipulate OSTree repositories.
+ */
+public final class Main {
+  static {
+    Logger.getGlobal().setLevel(WARNING);
+  }
+
+  public static void main(String[] args) {
+    int exitCode = new CommandLine(new MainCommand()).execute(args);
+    System.exit(exitCode);
+  }
+
+  private Main() {}
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/MainCommand.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/MainCommand.java
new file mode 100644
index 0000000..7b25dfc
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/MainCommand.java
@@ -0,0 +1,115 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool;
+
+import static java.util.logging.Level.*;
+
+import eu.mulk.jgvariant.ostree.Summary;
+import eu.mulk.jgvariant.tool.jsonb.*;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.util.logging.Logger;
+import org.jetbrains.annotations.VisibleForTesting;
+import picocli.AutoComplete;
+import picocli.CommandLine;
+import picocli.CommandLine.*;
+
+@Command(
+    name = "jgvariant",
+    mixinStandardHelpOptions = true,
+    description = "Manipulate files in GVariant format.",
+    subcommands = {MainCommand.OstreeCommand.class, AutoComplete.GenerateCompletion.class})
+final class MainCommand {
+
+  private static final Logger LOG = Logger.getLogger("eu.mulk.jgvariant.tool");
+
+  private static final Jsonb jsonb =
+      JsonbBuilder.newBuilder()
+          .withConfig(
+              new JsonbConfig()
+                  .withFormatting(true)
+                  .withAdapters(ChecksumAdapter.INSTANCE)
+                  .withSerializers(
+                      ByteStringSerializer.INSTANCE,
+                      ByteArraySerializer.INSTANCE,
+                      SignatureSerializer.INSTANCE,
+                      VariantSerializer.INSTANCE))
+          .build();
+
+  @Option(
+      names = {"-v", "--verbose"},
+      description = "Enable verbose logging.",
+      scope = CommandLine.ScopeType.INHERIT)
+  void setVerbose(boolean[] verbose) {
+    Logger.getGlobal()
+        .setLevel(
+            switch (verbose.length) {
+              case 0 -> WARNING;
+              case 1 -> INFO;
+              case 2 -> FINE;
+              default -> ALL;
+            });
+  }
+
+  @Command(
+      name = "ostree",
+      mixinStandardHelpOptions = true,
+      description = "Manipulate OSTree files.",
+      subcommands = {OstreeCommand.SummaryCommand.class})
+  static final class OstreeCommand {
+
+    @Command(
+        name = "summary",
+        mixinStandardHelpOptions = true,
+        description = "Manipulate OSTree summary files.")
+    static final class SummaryCommand extends BaseCommand {
+
+      @Command(mixinStandardHelpOptions = true)
+      void read(@Parameters(paramLabel = "<file>") File file) throws IOException {
+        LOG.fine(() -> "Reading file %s".formatted(file));
+        var fileBytes = ByteBuffer.wrap(Files.readAllBytes(fs().getPath(file.getPath())));
+        var decoder = Summary.decoder();
+        var thing = decoder.decode(fileBytes);
+        out().println(jsonb.toJson(thing));
+      }
+
+      SummaryCommand() {}
+    }
+
+    OstreeCommand() {}
+  }
+
+  @Command
+  abstract static class BaseCommand {
+
+    @Spec CommandLine.Model.CommandSpec spec;
+
+    @VisibleForTesting FileSystem fs = FileSystems.getDefault();
+
+    protected BaseCommand() {}
+
+    protected PrintWriter out() {
+      return spec.commandLine().getOut();
+    }
+
+    protected PrintWriter err() {
+      return spec.commandLine().getErr();
+    }
+
+    protected FileSystem fs() {
+      return fs;
+    }
+  }
+
+  MainCommand() {}
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteArraySerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteArraySerializer.java
new file mode 100644
index 0000000..7f80440
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteArraySerializer.java
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import java.util.HexFormat;
+
+@SuppressWarnings("java:S6548")
+public final class ByteArraySerializer implements JsonbSerializer<byte[]> {
+
+  public static final ByteArraySerializer INSTANCE = new ByteArraySerializer();
+
+  private ByteArraySerializer() {}
+
+  @Override
+  public void serialize(
+      byte[] o, JsonGenerator jsonGenerator, SerializationContext serializationContext) {
+    jsonGenerator.write(HexFormat.of().formatHex(o));
+  }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteStringSerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteStringSerializer.java
new file mode 100644
index 0000000..3537c3d
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteStringSerializer.java
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import eu.mulk.jgvariant.ostree.ByteString;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+
+@SuppressWarnings("java:S6548")
+public final class ByteStringSerializer implements JsonbSerializer<ByteString> {
+
+  public static final ByteStringSerializer INSTANCE = new ByteStringSerializer();
+
+  private ByteStringSerializer() {}
+
+  @Override
+  public void serialize(
+      ByteString o, JsonGenerator jsonGenerator, SerializationContext serializationContext) {
+    jsonGenerator.write(o.hex());
+  }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ChecksumAdapter.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ChecksumAdapter.java
new file mode 100644
index 0000000..58a8951
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ChecksumAdapter.java
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import eu.mulk.jgvariant.ostree.ByteString;
+import eu.mulk.jgvariant.ostree.Checksum;
+import jakarta.json.bind.adapter.JsonbAdapter;
+
+@SuppressWarnings("java:S6548")
+public final class ChecksumAdapter implements JsonbAdapter<Checksum, ByteString> {
+
+  public static final ChecksumAdapter INSTANCE = new ChecksumAdapter();
+
+  private ChecksumAdapter() {}
+
+  @Override
+  public ByteString adaptToJson(Checksum obj) throws Exception {
+    return obj.byteString();
+  }
+
+  @Override
+  public Checksum adaptFromJson(ByteString obj) throws Exception {
+    return new Checksum(obj);
+  }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/SignatureSerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/SignatureSerializer.java
new file mode 100644
index 0000000..dca1044
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/SignatureSerializer.java
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import eu.mulk.jgvariant.core.Signature;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+
+@SuppressWarnings("java:S6548")
+public final class SignatureSerializer implements JsonbSerializer<Signature> {
+
+  public static final SignatureSerializer INSTANCE = new SignatureSerializer();
+
+  private SignatureSerializer() {}
+
+  @Override
+  public void serialize(
+      Signature o, JsonGenerator jsonGenerator, SerializationContext serializationContext) {
+    jsonGenerator.write(o.toString());
+  }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/VariantSerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/VariantSerializer.java
new file mode 100644
index 0000000..99ff553
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/VariantSerializer.java
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import com.google.common.primitives.Bytes;
+import eu.mulk.jgvariant.core.Signature;
+import eu.mulk.jgvariant.core.Variant;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import java.text.ParseException;
+import java.util.List;
+
+@SuppressWarnings("java:S6548")
+public final class VariantSerializer implements JsonbSerializer<Variant> {
+
+  public static final VariantSerializer INSTANCE = new VariantSerializer();
+
+  private final ByteArraySerializer byteArraySerializer = ByteArraySerializer.INSTANCE;
+
+  private final Signature byteArraySignature;
+
+  private VariantSerializer() {
+    try {
+      byteArraySignature = Signature.parse("ay");
+    } catch (ParseException e) {
+      // impossible
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  @Override
+  @SuppressWarnings("unchecked")
+  public void serialize(Variant obj, JsonGenerator generator, SerializationContext ctx) {
+    if (obj.signature().equals(byteArraySignature)) {
+      byteArraySerializer.serialize(Bytes.toArray((List<Byte>) obj.value()), generator, ctx);
+    } else {
+      ctx.serialize(obj.value(), generator);
+    }
+  }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/package-info.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/package-info.java
new file mode 100644
index 0000000..da5c3ba
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/package-info.java
@@ -0,0 +1,8 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+@API(status = API.Status.INTERNAL)
+package eu.mulk.jgvariant.tool;
+
+import org.apiguardian.api.API;
diff --git a/jgvariant-tool/src/main/java/module-info.java b/jgvariant-tool/src/main/java/module-info.java
new file mode 100644
index 0000000..e66981c
--- /dev/null
+++ b/jgvariant-tool/src/main/java/module-info.java
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+module eu.mulk.jgvariant.tool {
+  requires transitive eu.mulk.jgvariant.ostree;
+  requires com.google.common;
+  requires info.picocli;
+  requires jakarta.json;
+  requires jakarta.json.bind;
+  requires java.logging;
+  requires static com.google.errorprone.annotations;
+  requires static org.apiguardian.api;
+  requires static org.jetbrains.annotations;
+
+  opens eu.mulk.jgvariant.tool to
+      info.picocli;
+
+  exports eu.mulk.jgvariant.tool;
+}