blob: 2bea23c8d7917e7cc7bd7336fd371a316ac86090 [file] [log] [blame]
// 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.core.Decoder;
import eu.mulk.jgvariant.core.Signature;
import eu.mulk.jgvariant.core.Variant;
import eu.mulk.jgvariant.ostree.ByteString;
import eu.mulk.jgvariant.ostree.Metadata;
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.channels.FileChannel;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.text.ParseException;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.IntStream;
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 BaseDecoderCommand<Summary> {
@Command(mixinStandardHelpOptions = true)
void read(@Parameters(paramLabel = "<file>", description = "Summary file to read") File file)
throws IOException {
read(file, Summary.decoder());
}
@Command(name = "add-static-delta", mixinStandardHelpOptions = true)
void addStaticDelta(
@Parameters(paramLabel = "<file>", description = "Summary file to manipulate.")
File summaryFile,
@Parameters(paramLabel = "<delta>", description = "Checksum of the static delta (hex).")
String delta,
@Parameters(paramLabel = "<to>", description = "Commit checksum the delta ends at (hex).")
String toCommit,
@Parameters(
paramLabel = "<from>",
arity = "0..1",
description = "Commit checksum the delta starts from (hex).")
String fromCommit)
throws IOException, ParseException {
var summaryDecoder = Summary.decoder();
var summary = decodeFile(summaryFile, summaryDecoder);
var staticDeltaMapSignature = Signature.parse("a{sv}");
var checksumSignature = Signature.parse("ay");
var metadata = summary.metadata();
var metadataFields = new LinkedHashMap<>(metadata.fields());
metadataFields.compute(
"ostree.static-deltas",
(k, v) -> {
Map<String, Variant> staticDeltas =
v != null
? new LinkedHashMap<>((Map<String, Variant>) v.value())
: new LinkedHashMap<>();
staticDeltas.put(
fromCommit != null ? fromCommit + "-" + toCommit : toCommit,
new Variant(checksumSignature, toByteList(ByteString.ofHex(delta).bytes())));
return new Variant(staticDeltaMapSignature, staticDeltas);
});
metadata = new Metadata(metadataFields);
summary = new Summary(summary.entries(), metadata);
encodeFile(summaryFile, summaryDecoder, summary);
}
private List<Byte> toByteList(byte[] bytes) {
return IntStream.range(0, bytes.length).mapToObj(i -> bytes[i]).toList();
}
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;
}
}
abstract static class BaseDecoderCommand<T> extends BaseCommand {
protected final void read(File file, Decoder<T> decoder) throws IOException {
var thing = decodeFile(file, decoder);
out().println(jsonb.toJson(thing));
}
protected final T decodeFile(File file, Decoder<T> decoder) throws IOException {
LOG.fine(() -> "Reading file %s".formatted(file));
var fileBytes = ByteBuffer.wrap(Files.readAllBytes(fs().getPath(file.getPath())));
return decoder.decode(fileBytes);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void encodeFile(File file, Decoder<T> decoder, T thing) throws IOException {
var thingBytes = decoder.encode(thing);
LOG.fine(() -> "Writing file %s".formatted(file));
try (var out = FileChannel.open(fs().getPath(file.getPath()), StandardOpenOption.WRITE)) {
out.write(thingBytes);
}
}
}
MainCommand() {}
}