blob: 2bea23c8d7917e7cc7bd7336fd371a316ac86090 [file] [log] [blame]
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +01001// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5package eu.mulk.jgvariant.tool;
6
7import static java.util.logging.Level.*;
8
Matthias Andreas Benkardec2c34a2024-03-02 13:48:04 +01009import eu.mulk.jgvariant.core.Decoder;
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +010010import eu.mulk.jgvariant.core.Signature;
11import eu.mulk.jgvariant.core.Variant;
12import eu.mulk.jgvariant.ostree.ByteString;
13import eu.mulk.jgvariant.ostree.Metadata;
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +010014import eu.mulk.jgvariant.ostree.Summary;
15import eu.mulk.jgvariant.tool.jsonb.*;
16import jakarta.json.bind.Jsonb;
17import jakarta.json.bind.JsonbBuilder;
18import jakarta.json.bind.JsonbConfig;
19import java.io.File;
20import java.io.IOException;
21import java.io.PrintWriter;
22import java.nio.ByteBuffer;
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +010023import java.nio.channels.FileChannel;
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +010024import java.nio.file.FileSystem;
25import java.nio.file.FileSystems;
26import java.nio.file.Files;
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +010027import java.nio.file.StandardOpenOption;
28import java.text.ParseException;
29import java.util.*;
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +010030import java.util.logging.Logger;
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +010031import java.util.stream.IntStream;
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +010032import org.jetbrains.annotations.VisibleForTesting;
33import picocli.AutoComplete;
34import picocli.CommandLine;
35import picocli.CommandLine.*;
36
37@Command(
38 name = "jgvariant",
39 mixinStandardHelpOptions = true,
40 description = "Manipulate files in GVariant format.",
41 subcommands = {MainCommand.OstreeCommand.class, AutoComplete.GenerateCompletion.class})
42final class MainCommand {
43
44 private static final Logger LOG = Logger.getLogger("eu.mulk.jgvariant.tool");
45
46 private static final Jsonb jsonb =
47 JsonbBuilder.newBuilder()
48 .withConfig(
49 new JsonbConfig()
50 .withFormatting(true)
51 .withAdapters(ChecksumAdapter.INSTANCE)
52 .withSerializers(
53 ByteStringSerializer.INSTANCE,
54 ByteArraySerializer.INSTANCE,
55 SignatureSerializer.INSTANCE,
56 VariantSerializer.INSTANCE))
57 .build();
58
59 @Option(
60 names = {"-v", "--verbose"},
61 description = "Enable verbose logging.",
62 scope = CommandLine.ScopeType.INHERIT)
63 void setVerbose(boolean[] verbose) {
64 Logger.getGlobal()
65 .setLevel(
66 switch (verbose.length) {
67 case 0 -> WARNING;
68 case 1 -> INFO;
69 case 2 -> FINE;
70 default -> ALL;
71 });
72 }
73
74 @Command(
75 name = "ostree",
76 mixinStandardHelpOptions = true,
77 description = "Manipulate OSTree files.",
78 subcommands = {OstreeCommand.SummaryCommand.class})
79 static final class OstreeCommand {
80
81 @Command(
82 name = "summary",
83 mixinStandardHelpOptions = true,
84 description = "Manipulate OSTree summary files.")
Matthias Andreas Benkardec2c34a2024-03-02 13:48:04 +010085 static final class SummaryCommand extends BaseDecoderCommand<Summary> {
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +010086
87 @Command(mixinStandardHelpOptions = true)
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +010088 void read(@Parameters(paramLabel = "<file>", description = "Summary file to read") File file)
89 throws IOException {
Matthias Andreas Benkardec2c34a2024-03-02 13:48:04 +010090 read(file, Summary.decoder());
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +010091 }
92
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +010093 @Command(name = "add-static-delta", mixinStandardHelpOptions = true)
94 void addStaticDelta(
95 @Parameters(paramLabel = "<file>", description = "Summary file to manipulate.")
96 File summaryFile,
97 @Parameters(paramLabel = "<delta>", description = "Checksum of the static delta (hex).")
98 String delta,
99 @Parameters(paramLabel = "<to>", description = "Commit checksum the delta ends at (hex).")
100 String toCommit,
101 @Parameters(
102 paramLabel = "<from>",
103 arity = "0..1",
104 description = "Commit checksum the delta starts from (hex).")
105 String fromCommit)
106 throws IOException, ParseException {
107 var summaryDecoder = Summary.decoder();
108
109 var summary = decodeFile(summaryFile, summaryDecoder);
110
111 var staticDeltaMapSignature = Signature.parse("a{sv}");
112 var checksumSignature = Signature.parse("ay");
113
114 var metadata = summary.metadata();
115 var metadataFields = new LinkedHashMap<>(metadata.fields());
116 metadataFields.compute(
117 "ostree.static-deltas",
118 (k, v) -> {
119 Map<String, Variant> staticDeltas =
120 v != null
121 ? new LinkedHashMap<>((Map<String, Variant>) v.value())
122 : new LinkedHashMap<>();
123 staticDeltas.put(
124 fromCommit != null ? fromCommit + "-" + toCommit : toCommit,
125 new Variant(checksumSignature, toByteList(ByteString.ofHex(delta).bytes())));
126 return new Variant(staticDeltaMapSignature, staticDeltas);
127 });
128 metadata = new Metadata(metadataFields);
129 summary = new Summary(summary.entries(), metadata);
130
131 encodeFile(summaryFile, summaryDecoder, summary);
132 }
133
134 private List<Byte> toByteList(byte[] bytes) {
135 return IntStream.range(0, bytes.length).mapToObj(i -> bytes[i]).toList();
136 }
137
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +0100138 SummaryCommand() {}
139 }
140
141 OstreeCommand() {}
142 }
143
144 @Command
145 abstract static class BaseCommand {
146
147 @Spec CommandLine.Model.CommandSpec spec;
148
149 @VisibleForTesting FileSystem fs = FileSystems.getDefault();
150
151 protected BaseCommand() {}
152
153 protected PrintWriter out() {
154 return spec.commandLine().getOut();
155 }
156
157 protected PrintWriter err() {
158 return spec.commandLine().getErr();
159 }
160
161 protected FileSystem fs() {
162 return fs;
163 }
164 }
165
Matthias Andreas Benkardec2c34a2024-03-02 13:48:04 +0100166 abstract static class BaseDecoderCommand<T> extends BaseCommand {
167
168 protected final void read(File file, Decoder<T> decoder) throws IOException {
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +0100169 var thing = decodeFile(file, decoder);
170 out().println(jsonb.toJson(thing));
171 }
172
173 protected final T decodeFile(File file, Decoder<T> decoder) throws IOException {
Matthias Andreas Benkardec2c34a2024-03-02 13:48:04 +0100174 LOG.fine(() -> "Reading file %s".formatted(file));
175 var fileBytes = ByteBuffer.wrap(Files.readAllBytes(fs().getPath(file.getPath())));
Matthias Andreas Benkard8e5f1f52024-03-02 14:54:34 +0100176 return decoder.decode(fileBytes);
177 }
178
179 @SuppressWarnings("ResultOfMethodCallIgnored")
180 protected final void encodeFile(File file, Decoder<T> decoder, T thing) throws IOException {
181 var thingBytes = decoder.encode(thing);
182
183 LOG.fine(() -> "Writing file %s".formatted(file));
184 try (var out = FileChannel.open(fs().getPath(file.getPath()), StandardOpenOption.WRITE)) {
185 out.write(thingBytes);
186 }
Matthias Andreas Benkardec2c34a2024-03-02 13:48:04 +0100187 }
188 }
189
Matthias Andreas Benkarda1e84432023-12-05 21:12:16 +0100190 MainCommand() {}
191}