Skip to content

Commit 00414f1

Browse files
fix(cmdline): replace System.exit in Supports with Callable<Integer>
Gemini review: System.exit() in Supports.run() abruptly terminates the JVM and prevents unit testing. Switch to Callable<Integer> so picocli handles the exit code via CommandLine.execute(). Also remove `required = true` from --client-id, --client-secret, and --platform-endpoint so `supports dpop` can run without auth credentials (it performs a local capability check, not a platform call). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 8bb0663 commit 00414f1

1 file changed

Lines changed: 55 additions & 53 deletions

File tree

cmdline/src/main/java/io/opentdf/platform/Command.java

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,20 @@
11
package io.opentdf.platform;
22

33
import com.google.gson.Gson;
4+
import com.google.gson.GsonBuilder;
45
import com.google.gson.JsonDeserializationContext;
56
import com.google.gson.JsonDeserializer;
67
import com.google.gson.JsonElement;
78
import com.google.gson.JsonObject;
89
import com.google.gson.JsonParseException;
10+
import com.google.gson.JsonSyntaxException;
11+
import com.google.gson.reflect.TypeToken;
912
import com.nimbusds.jose.JWSAlgorithm;
1013
import com.nimbusds.jose.jwk.Curve;
1114
import com.nimbusds.jose.jwk.JWK;
1215
import com.nimbusds.jose.jwk.KeyUse;
13-
import com.nimbusds.jose.jwk.RSAKey;
1416
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
1517
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
16-
import com.google.gson.GsonBuilder;
17-
import com.google.gson.reflect.TypeToken;
18-
19-
import java.security.cert.X509Certificate;
20-
import java.text.ParseException;
21-
import com.google.gson.JsonSyntaxException;
22-
import io.opentdf.platform.sdk.AssertionConfig;
23-
import io.opentdf.platform.sdk.AutoConfigureException;
24-
import io.opentdf.platform.sdk.Config;
25-
import io.opentdf.platform.sdk.KeyType;
26-
import io.opentdf.platform.sdk.SDK;
27-
import io.opentdf.platform.sdk.SDKBuilder;
28-
import picocli.CommandLine;
29-
import picocli.CommandLine.HelpCommand;
30-
import picocli.CommandLine.Option;
31-
32-
import javax.net.ssl.X509TrustManager;
3318
import java.io.BufferedInputStream;
3419
import java.io.BufferedOutputStream;
3520
import java.io.File;
@@ -47,13 +32,24 @@
4732
import java.security.spec.InvalidKeySpecException;
4833
import java.security.spec.PKCS8EncodedKeySpec;
4934
import java.security.spec.X509EncodedKeySpec;
35+
import java.text.ParseException;
5036
import java.util.ArrayList;
5137
import java.util.Base64;
5238
import java.util.List;
5339
import java.util.Map;
5440
import java.util.Optional;
5541
import java.util.UUID;
42+
import java.util.concurrent.Callable;
5643
import java.util.function.Consumer;
44+
import io.opentdf.platform.sdk.AssertionConfig;
45+
import io.opentdf.platform.sdk.AutoConfigureException;
46+
import io.opentdf.platform.sdk.Config;
47+
import io.opentdf.platform.sdk.KeyType;
48+
import io.opentdf.platform.sdk.SDK;
49+
import io.opentdf.platform.sdk.SDKBuilder;
50+
import picocli.CommandLine;
51+
import picocli.CommandLine.HelpCommand;
52+
import picocli.CommandLine.Option;
5753

5854
/**
5955
* Constants for the TDF command line tool.
@@ -67,36 +63,32 @@ class Versions {
6763
public static final String TDF_SPEC = "4.3.0";
6864
}
6965

70-
@CommandLine.Command(name = "tdf", subcommands = { HelpCommand.class, Command.Supports.class }, version = "{\"version\":\"" + Versions.SDK
71-
+ "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}")
66+
@CommandLine.Command(name = "tdf", subcommands = { HelpCommand.class,
67+
Command.Supports.class }, version = "{\"version\":\"" + Versions.SDK
68+
+ "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}")
7269
class Command {
7370

7471
@Option(names = { "-V", "--version" }, versionHelp = true, description = "display version info")
7572
boolean versionInfoRequested;
7673

7774
@CommandLine.Command(name = "supports", description = "Check if a feature is supported")
78-
static class Supports implements Runnable {
75+
static class Supports implements Callable<Integer> {
7976
@CommandLine.Parameters(index = "0", description = "Feature to check (e.g., dpop)")
8077
private String feature;
8178

8279
@Override
83-
public void run() {
84-
// Check if the requested feature is supported
85-
if ("dpop".equalsIgnoreCase(feature)) {
86-
// DPoP (RFC 9449) is supported
87-
System.exit(0);
88-
} else {
89-
// Unknown or unsupported feature
90-
System.exit(1);
91-
}
80+
public Integer call() {
81+
return "dpop".equalsIgnoreCase(feature) ? 0 : 1;
9282
}
9383
}
9484

9585
private static class AssertionKeyDeserializer implements JsonDeserializer<AssertionConfig.AssertionKey> {
9686
@Override
97-
public AssertionConfig.AssertionKey deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
87+
public AssertionConfig.AssertionKey deserialize(JsonElement json, java.lang.reflect.Type typeOfT,
88+
JsonDeserializationContext context) throws JsonParseException {
9889
JsonObject jsonObject = json.getAsJsonObject();
99-
AssertionConfig.AssertionKey assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.NotDefined, null);
90+
AssertionConfig.AssertionKey assertionKey = new AssertionConfig.AssertionKey(
91+
AssertionConfig.AssertionKeyAlg.NotDefined, null);
10092

10193
if (jsonObject.has("alg")) {
10294
assertionKey.alg = context.deserialize(jsonObject.get("alg"), AssertionConfig.AssertionKeyAlg.class);
@@ -112,7 +104,9 @@ public AssertionConfig.AssertionKey deserialize(JsonElement json, java.lang.refl
112104
}
113105
}
114106
if (jsonObject.has("x5c")) {
115-
assertionKey.x5c = context.deserialize(jsonObject.get("x5c"), new TypeToken<List<com.nimbusds.jose.util.Base64>>() {}.getType());
107+
assertionKey.x5c = context.deserialize(jsonObject.get("x5c"),
108+
new TypeToken<List<com.nimbusds.jose.util.Base64>>() {
109+
}.getType());
116110
}
117111

118112
return assertionKey;
@@ -130,7 +124,7 @@ private Gson buildGson() {
130124
private static final String PEM_HEADER = "-----BEGIN (.*)-----";
131125
private static final String PEM_FOOTER = "-----END (.*)-----";
132126

133-
@Option(names = { "--client-secret" }, required = true)
127+
@Option(names = { "--client-secret" })
134128
private String clientSecret;
135129

136130
@Option(names = { "-h", "--plaintext" }, defaultValue = "false")
@@ -139,20 +133,18 @@ private Gson buildGson() {
139133
@Option(names = { "-i", "--insecure" }, defaultValue = "false")
140134
private boolean insecure;
141135

142-
@Option(names = { "--client-id" }, required = true)
136+
@Option(names = { "--client-id" })
143137
private String clientId;
144138

145-
@Option(names = { "-p", "--platform-endpoint" }, required = true)
139+
@Option(names = { "-p", "--platform-endpoint" })
146140
private String platformEndpoint;
147141

148-
@Option(names = { "--dpop" }, arity = "0..1", fallbackValue = "",
149-
scope = CommandLine.ScopeType.INHERIT,
150-
description = "Enable DPoP (RFC 9449). Optional: specify algorithm (RS256, RS384, RS512, ES256, ES384, ES512). Default: RS256.")
142+
@Option(names = {
143+
"--dpop" }, arity = "0..1", fallbackValue = "", scope = CommandLine.ScopeType.INHERIT, description = "Enable DPoP (RFC 9449). Optional: specify algorithm (RS256, RS384, RS512, ES256, ES384, ES512). Default: RS256.")
151144
private String dpopAlg;
152145

153-
@Option(names = { "--dpop-key" },
154-
scope = CommandLine.ScopeType.INHERIT,
155-
description = "Enable DPoP using a PEM-encoded private key at <path>. Algorithm inferred from key type. Combinable with --dpop=<alg>.")
146+
@Option(names = {
147+
"--dpop-key" }, scope = CommandLine.ScopeType.INHERIT, description = "Enable DPoP using a PEM-encoded private key at <path>. Algorithm inferred from key type. Combinable with --dpop=<alg>.")
156148
private Path dpopKeyPath;
157149

158150
private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, boolean publicKey)
@@ -310,8 +302,10 @@ private SDK buildSDK() {
310302

311303
/**
312304
* Apply --dpop and --dpop-key options to the SDK builder.
313-
* --dpop-key loads a PEM private key; --dpop specifies the algorithm (default RS256).
314-
* If neither flag is set, the SDK auto-generates an ephemeral RSA-2048 DPoP key.
305+
* --dpop-key loads a PEM private key; --dpop specifies the algorithm (default
306+
* RS256).
307+
* If neither flag is set, the SDK auto-generates an ephemeral RSA-2048 DPoP
308+
* key.
315309
*/
316310
private void applyDPoPOptions(SDKBuilder builder) {
317311
try {
@@ -334,14 +328,21 @@ private void applyDPoPOptions(SDKBuilder builder) {
334328

335329
private static JWSAlgorithm parseAlgorithm(String alg) {
336330
switch (alg.toUpperCase()) {
337-
case "RS256": return JWSAlgorithm.RS256;
338-
case "RS384": return JWSAlgorithm.RS384;
339-
case "RS512": return JWSAlgorithm.RS512;
340-
case "ES256": return JWSAlgorithm.ES256;
341-
case "ES384": return JWSAlgorithm.ES384;
342-
case "ES512": return JWSAlgorithm.ES512;
343-
default: throw new RuntimeException("Unsupported DPoP algorithm: " + alg
344-
+ ". Supported: RS256, RS384, RS512, ES256, ES384, ES512");
331+
case "RS256":
332+
return JWSAlgorithm.RS256;
333+
case "RS384":
334+
return JWSAlgorithm.RS384;
335+
case "RS512":
336+
return JWSAlgorithm.RS512;
337+
case "ES256":
338+
return JWSAlgorithm.ES256;
339+
case "ES384":
340+
return JWSAlgorithm.ES384;
341+
case "ES512":
342+
return JWSAlgorithm.ES512;
343+
default:
344+
throw new RuntimeException("Unsupported DPoP algorithm: " + alg
345+
+ ". Supported: RS256, RS384, RS512, ES256, ES384, ES512");
345346
}
346347
}
347348

@@ -399,7 +400,8 @@ void decrypt(
399400
// try it as a file path
400401
try {
401402
String fileJson = new String(Files.readAllBytes(Paths.get(assertionVerificationInput)));
402-
assertionVerificationKeys = gson.fromJson(fileJson, Config.AssertionVerificationKeys.class);
403+
assertionVerificationKeys = gson.fromJson(fileJson,
404+
Config.AssertionVerificationKeys.class);
403405
} catch (JsonSyntaxException e2) {
404406
throw new RuntimeException("Failed to parse assertion verification keys from file", e2);
405407
} catch (Exception e3) {

0 commit comments

Comments
 (0)