feat(pki): add typed CLI operation foundation

Add a reusable PKI session and typed synchronous operation executor
shared by direct CLI commands and versioned sequential batch plans.

Provide deterministic references, structured output, failure policies and
safe lifecycle handling without introducing a scripting language or runtime.
This commit is contained in:
2026-08-04 01:13:49 +02:00
parent 64af4519f0
commit 3de6cd7a34
34 changed files with 3779 additions and 17 deletions

View File

@@ -35,6 +35,7 @@ package zeroecho;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -48,6 +49,7 @@ import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.help.HelpFormatter;
import zeroecho.sdk.util.BouncyCastleActivator;
import zeroecho.pki.cli.PkiCli;
/**
* ZeroEcho is a command-line utility for managing asymmetric keys and
@@ -102,6 +104,12 @@ public final class ZeroEcho {
public static void main(final String[] args) throws IOException {
final int errorCode = mainProcess(args);
if (args.length > 0 && "pki".equals(args[0])) {
if (errorCode != 0) {
System.exit(errorCode);
}
return;
}
if (errorCode == 0) {
System.out.println("OK");
} else {
@@ -119,6 +127,9 @@ public final class ZeroEcho {
* @throws IOException If the output could not be written
*/
public static int mainProcess(final String... args) throws IOException {
if (args.length > 0 && "pki".equals(args[0])) {
return PkiCli.execute(Arrays.copyOfRange(args, 1, args.length), System.out);
}
final Option KEM_OPTION = Option.builder("E").longOpt("kem").desc("KEM encryption/decryption").get();
final Option GUARD_OPTION = Option.builder("G").longOpt("guard")
.desc("multi-recipient encryption/decryption (keys+passwords), AES/ChaCha").get();

View File

@@ -0,0 +1,300 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import zeroecho.core.io.CancellationSignal;
import zeroecho.pki.application.PkiOperationFailure;
import zeroecho.pki.application.PkiOperationValue;
import zeroecho.pki.application.PkiSession;
import zeroecho.pki.application.PkiSessionConfiguration;
/** Composition root for the synchronous {@code zeroecho pki} command family. */
@SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidReassigningLoopVariables", "PMD.CloseResource", "PMD.DoNotUseThreads", "PMD.UseVarargs" })
public final class PkiCli {
private static final String CONFIG = "--config";
private static final String OUTPUT = "--output";
private static final String RUN = "run";
private static final int RUN_TOKEN_COUNT = 2;
private PkiCli() {
}
/**
* Executes one direct PKI operation or one sequential plan.
*
* @param arguments arguments following the {@code pki} command
* @param output exclusive result output stream
* @return stable PKI CLI process exit code
* @throws NullPointerException if an argument container is {@code null}
*/
public static int execute(String[] arguments, OutputStream output) {
return execute(arguments, output, PkiSession::open, () -> Thread.currentThread().isInterrupted());
}
/* default */ static int execute(String[] arguments, OutputStream output, SessionOpener opener,
CancellationSignal cancellation) {
Objects.requireNonNull(arguments, "arguments");
Objects.requireNonNull(output, "output");
Objects.requireNonNull(opener, "opener");
Objects.requireNonNull(cancellation, "cancellation");
PkiOutputRenderer renderer = new PkiOutputRenderer();
Invocation invocation;
try {
invocation = parseInvocation(arguments);
} catch (IllegalArgumentException failure) {
return renderFailure(renderer, PkiOutputMode.HUMAN, "INVALID_INVOCATION", "INVOCATION_INVALID",
PkiExitCodes.INVALID_INVOCATION, output);
}
if (invocation.help()) {
renderHelp(output);
return PkiExitCodes.SUCCESS;
}
PkiSessionConfiguration configuration;
try {
configuration = PkiCliConfiguration.read(invocation.configuration());
} catch (IOException | RuntimeException failure) {
return renderFailure(renderer, invocation.outputMode(), "CONFIGURATION_FAILURE", "CONFIGURATION_INVALID",
PkiExitCodes.CONFIGURATION_FAILURE, output);
}
PkiOperationRegistry registry = new PkiOperationRegistry();
PkiPlan plan;
try {
plan = invocation.batch() ? PkiPlanParser.read(invocation.plan(), registry.names())
: singlePlan(invocation, registry);
} catch (IOException | RuntimeException failure) {
return renderFailure(renderer, invocation.outputMode(), "INVALID_PLAN", "PLAN_INVALID",
PkiExitCodes.INVALID_INVOCATION, output);
}
PkiSession session;
try {
session = opener.open(configuration);
} catch (IllegalArgumentException failure) {
return renderFailure(renderer, invocation.outputMode(), "CONFIGURATION_FAILURE", "CONFIGURATION_INVALID",
PkiExitCodes.CONFIGURATION_FAILURE, output);
} catch (RuntimeException failure) {
return renderFailure(renderer, invocation.outputMode(), "RESOURCE_FAILURE", "SESSION_OPEN_FAILED",
PkiExitCodes.RESOURCE_FAILURE, output);
}
PkiPlanExecution execution;
try {
execution = new PkiPlanExecutor(registry).execute(plan, session, cancellation);
} catch (RuntimeException failure) {
execution = internalFailure(plan);
}
try {
session.close();
} catch (Exception failure) {
execution = execution.withPlanFailure(PkiOperationFailure.RESOURCE_FAILURE, "SESSION_CLOSE_FAILED");
}
try {
renderer.render(execution, invocation.outputMode(), invocation.batch(), output);
} catch (IOException failure) {
return PkiExitCodes.RESOURCE_FAILURE;
}
return PkiExitCodes.from(execution);
}
@SuppressWarnings("PMD.CyclomaticComplexity")
private static Invocation parseInvocation(String[] arguments) {
if (arguments.length == 0) {
throw new IllegalArgumentException("PKI command is missing");
}
Path configuration = null;
PkiOutputMode outputMode = PkiOutputMode.HUMAN;
List<String> commandTokens = new ArrayList<>();
boolean help = false;
boolean outputSelected = false;
for (int index = 0; index < arguments.length; index++) {
String argument = Objects.requireNonNull(arguments[index], "argument");
if (CONFIG.equals(argument)) {
if (configuration != null) {
throw new IllegalArgumentException("PKI configuration option is duplicated");
}
configuration = Path.of(requireValue(arguments, ++index));
} else if (OUTPUT.equals(argument)) {
if (outputSelected) {
throw new IllegalArgumentException("PKI output option is duplicated");
}
outputMode = parseOutput(requireValue(arguments, ++index));
outputSelected = true;
} else if ("--help".equals(argument) || "-h".equals(argument)) {
help = true;
} else {
commandTokens.add(argument);
}
}
if (help) {
return new Invocation(null, outputMode, false, null, null, null, true);
}
if (configuration == null || commandTokens.isEmpty()) {
throw new IllegalArgumentException("PKI configuration or operation is missing");
}
String command = commandTokens.get(0);
if (RUN.equals(command)) {
if (commandTokens.size() != RUN_TOKEN_COUNT) {
throw new IllegalArgumentException("Plan path is invalid");
}
return new Invocation(configuration, outputMode, true, Path.of(commandTokens.get(1)), null, null, false);
}
PkiOperationValue.ObjectValue operationArguments = directArguments(commandTokens.subList(1,
commandTokens.size()));
return new Invocation(configuration, outputMode, false, null, command, operationArguments, false);
}
private static PkiOperationValue.ObjectValue directArguments(List<String> tokens) {
if ((tokens.size() & 1) != 0) {
throw new IllegalArgumentException("Operation arguments must be option-value pairs");
}
Map<String, PkiOperationValue> arguments = new LinkedHashMap<>();
for (int index = 0; index < tokens.size(); index += 2) {
String name = argumentName(tokens.get(index));
String raw = tokens.get(index + 1);
PkiOperationValue value = "limit".equals(name) ? new PkiOperationValue.IntegerValue(parseLong(raw))
: new PkiOperationValue.Text(raw);
if (arguments.putIfAbsent(name, value) != null) {
throw new IllegalArgumentException("Operation argument is duplicated");
}
}
return new PkiOperationValue.ObjectValue(arguments);
}
private static String argumentName(String option) {
return switch (option) {
case "--profile-file" -> "profileFile";
case "--credential-id" -> "credentialId";
case "--publication-id" -> "publicationId";
case "--reason" -> "reason";
case "--limit" -> "limit";
default -> throw new IllegalArgumentException("Unknown PKI operation argument");
};
}
private static PkiPlan singlePlan(Invocation invocation, PkiOperationRegistry registry) throws IOException {
if (!registry.names().contains(invocation.operation())) {
throw new IllegalArgumentException("Unknown PKI operation");
}
Path base = Path.of(".").toAbsolutePath().normalize();
PkiPlan.Step step = new PkiPlan.Step("command", invocation.operation(), invocation.arguments());
return new PkiPlan(PkiPlan.CURRENT_VERSION, PkiPlan.FailurePolicy.FAIL_FAST, List.of(step), base);
}
private static PkiPlanExecution internalFailure(PkiPlan plan) {
List<PkiPlanExecution.StepResult> results = new ArrayList<>(plan.steps().size());
boolean first = true;
for (PkiPlan.Step step : plan.steps()) {
if (first) {
results.add(PkiPlanExecution.StepResult.failure(step.id(), step.operation(),
PkiOperationFailure.INTERNAL_FAILURE, "INTERNAL_FAILURE"));
first = false;
} else {
results.add(PkiPlanExecution.StepResult.skipped(step.id(), step.operation(), "FAIL_FAST"));
}
}
return new PkiPlanExecution(results);
}
private static int renderFailure(PkiOutputRenderer renderer, PkiOutputMode mode, String category, String code,
int exitCode, OutputStream output) {
try {
renderer.renderFailure(mode, category, code, output);
} catch (IOException ignored) {
return PkiExitCodes.RESOURCE_FAILURE;
}
return exitCode;
}
private static void renderHelp(OutputStream output) {
java.io.PrintWriter writer = new java.io.PrintWriter(output, false, java.nio.charset.StandardCharsets.UTF_8);
writer.println("Usage: zeroecho pki <operation> --config <file> [--output human|json] [arguments]");
writer.println(" zeroecho pki run <plan-file> --config <file> [--output human|json]");
writer.println("Operations:");
writer.println(" configuration.validate");
writer.println(" profile.validate --profile-file <file>");
writer.println(" credential.inspect --credential-id <id>");
writer.println(" credential.revoke --credential-id <id> --reason <permanent-reason>");
writer.println(" revocation.history --credential-id <id> --limit <1..1000>");
writer.println(" publication.inspect --publication-id <id>");
writer.flush();
}
private static PkiOutputMode parseOutput(String raw) {
return switch (raw) {
case "human" -> PkiOutputMode.HUMAN;
case "json" -> PkiOutputMode.JSON;
default -> throw new IllegalArgumentException("Output mode is invalid");
};
}
private static String requireValue(String[] arguments, int index) {
if (index >= arguments.length) {
throw new IllegalArgumentException("Option value is missing");
}
return arguments[index];
}
private static long parseLong(String raw) {
try {
return Long.parseLong(raw);
} catch (NumberFormatException failure) {
throw new IllegalArgumentException("Numeric operation argument is invalid", failure);
}
}
/** Lifecycle construction seam for CLI orchestration tests. */
@FunctionalInterface
@SuppressWarnings("PMD.CommentDefaultAccessModifier")
interface SessionOpener {
/** Opens one lifecycle-owned backend session. */
PkiSession open(PkiSessionConfiguration configuration);
}
private record Invocation(Path configuration, PkiOutputMode outputMode, boolean batch, Path plan,
String operation, PkiOperationValue.ObjectValue arguments, boolean help) {
}
}

View File

@@ -0,0 +1,114 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import zeroecho.pki.application.PkiOperationValue;
import zeroecho.pki.application.PkiSessionConfiguration;
import zeroecho.pki.spi.ProviderConfig;
/** Strict versioned CLI configuration loader. */
final class PkiCliConfiguration {
private static final Set<String> ROOT_FIELDS = Set.of("version", "store", "audit");
private static final Set<String> PROVIDER_FIELDS = Set.of("provider", "properties");
private static final String STDOUT_PROVIDER = "stdout";
private PkiCliConfiguration() {
}
/* default */ static PkiSessionConfiguration read(Path path) throws java.io.IOException {
byte[] document = PkiCliFiles.readRegularFile(path, PkiCliJson.MAXIMUM_DOCUMENT_BYTES);
PkiOperationValue.ObjectValue root = object(PkiCliJson.parse(document));
requireExactFields(root.fields(), ROOT_FIELDS);
int version = Math.toIntExact(integer(required(root, "version")));
ProviderConfig store = provider(required(root, "store"));
ProviderConfig audit = provider(required(root, "audit"));
if (STDOUT_PROVIDER.equals(audit.backendId())) {
throw new IllegalArgumentException("CLI audit provider must not write to standard output");
}
return new PkiSessionConfiguration(version, store, audit);
}
private static ProviderConfig provider(PkiOperationValue value) {
PkiOperationValue.ObjectValue object = object(value);
requireExactFields(object.fields(), PROVIDER_FIELDS);
String provider = text(required(object, "provider"));
PkiOperationValue.ObjectValue properties = object(required(object, "properties"));
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<String, PkiOperationValue> entry : properties.fields().entrySet()) {
result.put(entry.getKey(), text(entry.getValue()));
}
return new ProviderConfig(provider, result);
}
/* default */ static PkiOperationValue required(PkiOperationValue.ObjectValue object, String name) {
PkiOperationValue value = object.fields().get(name);
if (value == null) {
throw new IllegalArgumentException("Required CLI field is missing");
}
return value;
}
/* default */ static PkiOperationValue.ObjectValue object(PkiOperationValue value) {
if (value instanceof PkiOperationValue.ObjectValue object) {
return object;
}
throw new IllegalArgumentException("CLI field has the wrong type");
}
/* default */ static String text(PkiOperationValue value) {
if (value instanceof PkiOperationValue.Text text) {
return text.value();
}
throw new IllegalArgumentException("CLI field has the wrong type");
}
/* default */ static long integer(PkiOperationValue value) {
if (value instanceof PkiOperationValue.IntegerValue integer) {
return integer.value();
}
throw new IllegalArgumentException("CLI field has the wrong type");
}
/* default */ static void requireExactFields(Map<String, PkiOperationValue> actual, Set<String> expected) {
if (!actual.keySet().equals(expected)) {
throw new IllegalArgumentException("CLI document fields are invalid");
}
}
}

View File

@@ -0,0 +1,92 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
/** Bounded regular-file input used by the CLI trust boundary. */
final class PkiCliFiles {
private static final int BUFFER_BYTES = 8_192;
private PkiCliFiles() {
}
/* default */ static byte[] readRegularFile(Path path, int maximumBytes) throws IOException {
Path exact = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
if (maximumBytes <= 0) {
throw new IllegalArgumentException("maximumBytes must be positive");
}
int readLimit = Math.addExact(maximumBytes, 1);
BasicFileAttributes attributes = Files.readAttributes(exact, BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
if (!attributes.isRegularFile() || attributes.isSymbolicLink() || attributes.size() < 0L
|| attributes.size() > maximumBytes) {
throw new IOException("CLI input file is invalid");
}
ByteArrayOutputStream output = new ByteArrayOutputStream((int) Math.min(attributes.size(), BUFFER_BYTES));
byte[] buffer = new byte[BUFFER_BYTES];
int total = 0;
try (SeekableByteChannel channel = Files.newByteChannel(exact, StandardOpenOption.READ,
LinkOption.NOFOLLOW_LINKS);
InputStream input = Channels.newInputStream(channel)) {
while (true) {
int remaining = readLimit - total;
int count = input.read(buffer, 0, Math.min(buffer.length, remaining));
if (count < 0) {
break;
}
if (count == 0) {
continue;
}
output.write(buffer, 0, count);
total = Math.addExact(total, count);
if (total > maximumBytes) {
throw new IOException("CLI input file is too large");
}
}
}
return output.toByteArray();
}
}

View File

@@ -0,0 +1,147 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.ObjectReadContext;
import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.StreamReadFeature;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonFactoryBuilder;
import tools.jackson.core.json.JsonReadFeature;
import zeroecho.pki.application.PkiOperationValue;
/** Strict bounded JSON reader for CLI configuration and operation plans. */
final class PkiCliJson {
/* default */ static final int MAXIMUM_DOCUMENT_BYTES = 1_048_576;
/* default */ static final int MAXIMUM_CONTAINER_ENTRIES = 2_048;
private static final int MAXIMUM_DEPTH = 16;
private static final int MAXIMUM_STRING_LENGTH = 4_096;
private static final JsonFactory JSON_FACTORY = createFactory();
private PkiCliJson() {
}
/* default */ static PkiOperationValue parse(byte[] document) {
if (document == null || document.length == 0 || document.length > MAXIMUM_DOCUMENT_BYTES || hasBom(document)) {
throw invalid();
}
try (JsonParser parser = JSON_FACTORY.createParser(ObjectReadContext.empty(), document, 0, document.length)) {
JsonToken first = parser.nextToken();
if (first == null) {
throw invalid();
}
PkiOperationValue value = readValue(parser, first);
if (parser.nextToken() != null) {
throw invalid();
}
return value;
} catch (JacksonException failure) {
throw invalid(failure);
}
}
private static PkiOperationValue readValue(JsonParser parser, JsonToken token) {
return switch (token) {
case START_OBJECT -> readObject(parser);
case START_ARRAY -> readArray(parser);
case VALUE_STRING -> new PkiOperationValue.Text(parser.getString());
case VALUE_NUMBER_INT -> new PkiOperationValue.IntegerValue(parser.getLongValue());
case VALUE_TRUE -> new PkiOperationValue.BooleanValue(true);
case VALUE_FALSE -> new PkiOperationValue.BooleanValue(false);
default -> throw invalid();
};
}
private static PkiOperationValue.ObjectValue readObject(JsonParser parser) {
Map<String, PkiOperationValue> fields = new LinkedHashMap<>();
while (parser.nextToken() != JsonToken.END_OBJECT) {
if (parser.currentToken() != JsonToken.PROPERTY_NAME || fields.size() >= MAXIMUM_CONTAINER_ENTRIES) {
throw invalid();
}
String name = parser.currentName();
JsonToken valueToken = parser.nextToken();
if (valueToken == null || fields.putIfAbsent(name, readValue(parser, valueToken)) != null) {
throw invalid();
}
}
return new PkiOperationValue.ObjectValue(fields);
}
private static PkiOperationValue.ListValue readArray(JsonParser parser) {
List<PkiOperationValue> values = new ArrayList<>();
JsonToken token;
while ((token = parser.nextToken()) != JsonToken.END_ARRAY) {
if (token == null || values.size() >= MAXIMUM_CONTAINER_ENTRIES) {
throw invalid();
}
values.add(readValue(parser, token));
}
return new PkiOperationValue.ListValue(values);
}
private static JsonFactory createFactory() {
StreamReadConstraints constraints = StreamReadConstraints.builder().maxNestingDepth(MAXIMUM_DEPTH)
.maxDocumentLength(MAXIMUM_DOCUMENT_BYTES).maxTokenCount(32_768).maxNumberLength(20)
.maxStringLength(MAXIMUM_STRING_LENGTH).maxNameLength(64).build();
JsonFactoryBuilder builder = JsonFactory.builder().streamReadConstraints(constraints)
.enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION).disable(StreamReadFeature.AUTO_CLOSE_SOURCE);
for (JsonReadFeature feature : JsonReadFeature.values()) {
builder.disable(feature);
}
return builder.build();
}
private static boolean hasBom(byte[] document) {
return document.length >= 3 && document[0] == (byte) 0xef && document[1] == (byte) 0xbb
&& document[2] == (byte) 0xbf;
}
private static IllegalArgumentException invalid() {
return new IllegalArgumentException("CLI JSON document is invalid");
}
private static IllegalArgumentException invalid(JacksonException failure) {
return new IllegalArgumentException("CLI JSON document is invalid", failure);
}
}

View File

@@ -0,0 +1,76 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import zeroecho.pki.application.PkiOperationFailure;
/** Stable process exit-code mapping for the PKI command family. */
final class PkiExitCodes {
/* default */ static final int SUCCESS = 0;
/* default */ static final int INVALID_INVOCATION = 2;
/* default */ static final int CONFIGURATION_FAILURE = 3;
/* default */ static final int VALIDATION_OR_POLICY = 4;
/* default */ static final int NOT_FOUND_OR_CONFLICT = 5;
/* default */ static final int RECOVERY_REQUIRED = 6;
/* default */ static final int EXTERNAL_OUTCOME_UNKNOWN = 7;
/* default */ static final int RESOURCE_FAILURE = 8;
/* default */ static final int INTERNAL_FAILURE = 9;
/* default */ static final int CANCELLED = 130;
private PkiExitCodes() {
}
/* default */ static int from(PkiPlanExecution execution) {
int result = execution.planFailure().map(PkiExitCodes::from).orElse(SUCCESS);
for (PkiPlanExecution.StepResult step : execution.steps()) {
if (step.failure().isPresent()) {
result = Math.max(result, from(step.failure().orElseThrow()));
}
}
return result == SUCCESS && !execution.succeeded() ? VALIDATION_OR_POLICY : result;
}
private static int from(PkiOperationFailure failure) {
return switch (failure) {
case VALIDATION_FAILURE, POLICY_REJECTION -> VALIDATION_OR_POLICY;
case NOT_FOUND, CONFLICT -> NOT_FOUND_OR_CONFLICT;
case RECOVERY_REQUIRED -> RECOVERY_REQUIRED;
case EXTERNAL_OUTCOME_UNKNOWN -> EXTERNAL_OUTCOME_UNKNOWN;
case RESOURCE_FAILURE -> RESOURCE_FAILURE;
case INTERNAL_FAILURE -> INTERNAL_FAILURE;
case CANCELLED -> CANCELLED;
};
}
}

View File

@@ -0,0 +1,145 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.io.IOException;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
import zeroecho.pki.api.revocation.RevocationReason;
import zeroecho.pki.application.PkiOperation;
import zeroecho.pki.application.PkiOperationValue;
/** Explicit stable operation-name to typed-request binding registry. */
@SuppressWarnings("PMD.UnusedFormalParameter")
final class PkiOperationRegistry {
private static final String CREDENTIAL_ID = "credentialId";
private final Map<String, Binding> bindings;
/* default */ PkiOperationRegistry() {
Map<String, Binding> configured = new LinkedHashMap<>();
add(configured, PkiOperation.ValidateConfiguration.NAME, this::configuration);
add(configured, PkiOperation.ValidateProfile.NAME, this::profile);
add(configured, PkiOperation.InspectCredential.NAME, this::credential);
add(configured, PkiOperation.RevokeCredential.NAME, this::revoke);
add(configured, PkiOperation.ReadRevocationHistory.NAME, this::history);
add(configured, PkiOperation.InspectPublication.NAME, this::publication);
bindings = Map.copyOf(configured);
}
/* default */ Set<String> names() {
return bindings.keySet();
}
/* default */ PkiOperation bind(String name, PkiOperationValue.ObjectValue arguments, Path baseDirectory)
throws IOException {
Binding binding = bindings.get(name);
if (binding == null) {
throw new IllegalArgumentException("Unknown PKI operation");
}
return binding.bind(arguments, baseDirectory);
}
private PkiOperation configuration(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
requireFields(arguments, Set.of());
return new PkiOperation.ValidateConfiguration();
}
private PkiOperation profile(PkiOperationValue.ObjectValue arguments, Path baseDirectory) throws IOException {
requireFields(arguments, Set.of("profileFile"));
Path path = resolve(baseDirectory, text(arguments, "profileFile"));
byte[] document = PkiCliFiles.readRegularFile(path, CertificateProfileDocumentCodec.MAXIMUM_DOCUMENT_BYTES);
return new PkiOperation.ValidateProfile(document);
}
private PkiOperation credential(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
requireFields(arguments, Set.of(CREDENTIAL_ID));
return new PkiOperation.InspectCredential(new PkiId(text(arguments, CREDENTIAL_ID)));
}
private PkiOperation revoke(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
requireFields(arguments, Set.of(CREDENTIAL_ID, "reason"));
RevocationReason reason = RevocationReason.valueOf(text(arguments, "reason"));
return new PkiOperation.RevokeCredential(new PkiId(text(arguments, CREDENTIAL_ID)), reason);
}
private PkiOperation history(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
requireFields(arguments, Set.of(CREDENTIAL_ID, "limit"));
int limit = Math.toIntExact(integer(arguments, "limit"));
return new PkiOperation.ReadRevocationHistory(new PkiId(text(arguments, CREDENTIAL_ID)), limit);
}
private PkiOperation publication(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
requireFields(arguments, Set.of("publicationId"));
return new PkiOperation.InspectPublication(new PkiId(text(arguments, "publicationId")));
}
private static void add(Map<String, Binding> target, String name, Binding binding) {
if (target.putIfAbsent(name, binding) != null) {
throw new IllegalStateException("Duplicate PKI operation identity");
}
}
private static void requireFields(PkiOperationValue.ObjectValue arguments, Set<String> expected) {
if (!arguments.fields().keySet().equals(expected)) {
throw new IllegalArgumentException("PKI operation arguments are invalid");
}
}
private static String text(PkiOperationValue.ObjectValue arguments, String name) {
return PkiCliConfiguration.text(PkiCliConfiguration.required(arguments, name));
}
private static long integer(PkiOperationValue.ObjectValue arguments, String name) {
return PkiCliConfiguration.integer(PkiCliConfiguration.required(arguments, name));
}
private static Path resolve(Path baseDirectory, String value) {
Path supplied = Path.of(value);
return (supplied.isAbsolute() ? supplied : baseDirectory.resolve(supplied)).toAbsolutePath().normalize();
}
/** One explicit typed binding. */
@FunctionalInterface
private interface Binding {
/** Binds validated structured arguments to one typed operation. */
PkiOperation bind(PkiOperationValue.ObjectValue arguments, Path baseDirectory) throws IOException;
}
}

View File

@@ -0,0 +1,40 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
/** Supported administrator output representations. */
enum PkiOutputMode {
HUMAN,
JSON
}

View File

@@ -0,0 +1,188 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.ObjectWriteContext;
import tools.jackson.core.json.JsonFactory;
import zeroecho.pki.application.PkiOperationResult;
import zeroecho.pki.application.PkiOperationValue;
/** Deterministic safe human and versioned machine result renderer. */
final class PkiOutputRenderer {
private static final JsonFactory JSON_FACTORY = JsonFactory.builder().build();
/* default */ void render(PkiPlanExecution execution, PkiOutputMode mode, boolean batch, OutputStream output)
throws IOException {
if (mode == PkiOutputMode.JSON) {
renderJson(execution, batch, output);
} else {
renderHuman(execution, output);
}
}
/* default */ void renderFailure(PkiOutputMode mode, String category, String code, OutputStream output)
throws IOException {
if (mode == PkiOutputMode.HUMAN) {
PrintWriter writer = new PrintWriter(output, false, StandardCharsets.UTF_8);
writer.println("PKI command: FAILED");
writer.println("category: " + category);
writer.println("code: " + code);
writer.flush();
return;
}
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
try (JsonGenerator generator = JSON_FACTORY.createGenerator(ObjectWriteContext.empty(), encoded)) {
generator.writeStartObject();
generator.writeNumberProperty("version", 1);
generator.writeStringProperty("status", "FAILED");
generator.writeStringProperty("category", category);
generator.writeStringProperty("code", code);
generator.writeEndObject();
} catch (JacksonException failure) {
throw new IOException("Machine output generation failed", failure);
}
output.write(encoded.toByteArray());
output.write('\n');
output.flush();
}
private static void renderHuman(PkiPlanExecution execution, OutputStream output) {
PrintWriter writer = new PrintWriter(output, false, StandardCharsets.UTF_8);
writer.println("PKI plan: " + (execution.succeeded() ? "SUCCEEDED" : "FAILED"));
if (execution.planFailure().isPresent()) {
writer.println("code: " + execution.planCode().orElseThrow());
}
for (PkiPlanExecution.StepResult step : execution.steps()) {
writer.println(step.id() + " " + step.operation() + " " + step.status());
if (step.output().isPresent()) {
for (Map.Entry<String, PkiOperationValue> field : step.output().orElseThrow().fields().entrySet()) {
writer.println(" " + field.getKey() + ": " + human(field.getValue()));
}
} else {
step.code().ifPresent(code -> writer.println(" code: " + code));
}
}
writer.flush();
}
private static void renderJson(PkiPlanExecution execution, boolean batch, OutputStream output) throws IOException {
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
try (JsonGenerator generator = JSON_FACTORY.createGenerator(ObjectWriteContext.empty(), encoded)) {
generator.writeStartObject();
generator.writeNumberProperty("version", 1);
generator.writeStringProperty("mode", batch ? "batch" : "single");
generator.writeStringProperty("status", execution.succeeded() ? "SUCCEEDED" : "FAILED");
if (execution.planFailure().isPresent()) {
generator.writeStringProperty("failure", execution.planFailure().orElseThrow().name());
generator.writeStringProperty("code", execution.planCode().orElseThrow());
}
generator.writeArrayPropertyStart("steps");
for (PkiPlanExecution.StepResult step : execution.steps()) {
writeStep(generator, step);
}
generator.writeEndArray();
generator.writeEndObject();
} catch (JacksonException failure) {
throw new IOException("Machine output generation failed", failure);
}
output.write(encoded.toByteArray());
output.write('\n');
output.flush();
}
private static void writeStep(JsonGenerator generator, PkiPlanExecution.StepResult step) {
generator.writeStartObject();
generator.writeStringProperty("id", step.id());
generator.writeStringProperty("operation", step.operation());
generator.writeStringProperty("status", step.status().name());
if (step.output().isPresent()) {
generator.writeObjectPropertyStart("output");
PkiOperationResult result = step.output().orElseThrow();
for (Map.Entry<String, PkiOperationValue> field : result.fields().entrySet()) {
generator.writeName(field.getKey());
writeValue(generator, field.getValue());
}
generator.writeEndObject();
} else {
if (step.failure().isPresent()) {
generator.writeStringProperty("failure", step.failure().orElseThrow().name());
}
generator.writeStringProperty("code", step.code().orElseThrow());
}
generator.writeEndObject();
}
private static void writeValue(JsonGenerator generator, PkiOperationValue value) {
switch (value) {
case PkiOperationValue.Text text -> generator.writeString(text.value());
case PkiOperationValue.IntegerValue integer -> generator.writeNumber(integer.value());
case PkiOperationValue.BooleanValue bool -> generator.writeBoolean(bool.value());
case PkiOperationValue.ObjectValue object -> {
generator.writeStartObject();
for (Map.Entry<String, PkiOperationValue> entry : object.fields().entrySet()) {
generator.writeName(entry.getKey());
writeValue(generator, entry.getValue());
}
generator.writeEndObject();
}
case PkiOperationValue.ListValue list -> {
generator.writeStartArray();
for (PkiOperationValue entry : list.values()) {
writeValue(generator, entry);
}
generator.writeEndArray();
}
}
}
private static String human(PkiOperationValue value) {
return switch (value) {
case PkiOperationValue.Text text -> text.value();
case PkiOperationValue.IntegerValue integer -> Long.toString(integer.value());
case PkiOperationValue.BooleanValue bool -> Boolean.toString(bool.value());
case PkiOperationValue.ObjectValue object -> "object(" + object.fields().size() + ")";
case PkiOperationValue.ListValue list -> "list(" + list.values().size() + ")";
};
}
}

View File

@@ -0,0 +1,70 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import zeroecho.pki.application.PkiOperationValue;
/** Immutable validated sequential operation plan. */
record PkiPlan(int version, FailurePolicy failurePolicy, List<Step> steps, Path baseDirectory) {
/* default */ static final int CURRENT_VERSION = 1;
PkiPlan {
if (version != CURRENT_VERSION || steps == null || steps.isEmpty()) {
throw new IllegalArgumentException("Plan version or steps are invalid");
}
Objects.requireNonNull(failurePolicy, "failurePolicy");
steps = List.copyOf(steps);
baseDirectory = Objects.requireNonNull(baseDirectory, "baseDirectory").toAbsolutePath().normalize();
}
/** Supported sequential failure policies. */
/* default */ enum FailurePolicy {
FAIL_FAST,
CONTINUE_INDEPENDENT
}
/** One validated ordered plan step. */
/* default */ record Step(String id, String operation, PkiOperationValue.ObjectValue arguments) {
Step {
if (id == null || operation == null || arguments == null) {
throw new IllegalArgumentException("Plan step is incomplete");
}
}
}
}

View File

@@ -0,0 +1,115 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import zeroecho.pki.application.PkiOperationFailure;
import zeroecho.pki.application.PkiOperationResult;
/** Ordered complete result of one single-command or batch plan execution. */
record PkiPlanExecution(List<StepResult> steps, Optional<PkiOperationFailure> planFailure, Optional<String> planCode) {
/* default */ PkiPlanExecution {
steps = List.copyOf(steps);
Objects.requireNonNull(planFailure, "planFailure");
Objects.requireNonNull(planCode, "planCode");
if (steps.isEmpty()) {
throw new IllegalArgumentException("execution requires at least one step");
}
if (planFailure.isPresent() != planCode.isPresent()) {
throw new IllegalArgumentException("plan failure state is inconsistent");
}
}
/* default */ PkiPlanExecution(List<StepResult> steps) {
this(steps, Optional.empty(), Optional.empty());
}
/* default */ boolean succeeded() {
return planFailure.isEmpty() && steps.stream().allMatch(step -> step.status() == StepStatus.SUCCEEDED);
}
/* default */ PkiPlanExecution withPlanFailure(PkiOperationFailure failure, String code) {
return new PkiPlanExecution(steps, Optional.of(failure), Optional.of(code));
}
/** Step completion state. */
/* default */ enum StepStatus {
SUCCEEDED,
FAILED,
SKIPPED
}
/** One safe ordered step result. */
/* default */ record StepResult(String id, String operation, StepStatus status,
Optional<PkiOperationResult> output,
Optional<PkiOperationFailure> failure, Optional<String> code) {
StepResult {
if (id == null || id.isBlank() || operation == null || operation.isBlank()) {
throw new IllegalArgumentException("step result identity is invalid");
}
Objects.requireNonNull(status, "status");
Objects.requireNonNull(output, "output");
Objects.requireNonNull(failure, "failure");
Objects.requireNonNull(code, "code");
boolean valid = switch (status) {
case SUCCEEDED -> output.isPresent() && failure.isEmpty() && code.isEmpty();
case FAILED -> output.isEmpty() && failure.isPresent() && code.isPresent();
case SKIPPED -> output.isEmpty() && failure.isEmpty() && code.isPresent();
};
if (!valid) {
throw new IllegalArgumentException("step result state is inconsistent");
}
}
/* default */ static StepResult success(String id, String operation, PkiOperationResult result) {
return new StepResult(id, operation, StepStatus.SUCCEEDED, Optional.of(result), Optional.empty(),
Optional.empty());
}
/* default */ static StepResult failure(String id, String operation, PkiOperationFailure failure,
String code) {
return new StepResult(id, operation, StepStatus.FAILED, Optional.empty(), Optional.of(failure),
Optional.of(code));
}
/* default */ static StepResult skipped(String id, String operation, String code) {
return new StepResult(id, operation, StepStatus.SKIPPED, Optional.empty(), Optional.empty(),
Optional.of(code));
}
}
}

View File

@@ -0,0 +1,198 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
import java.util.regex.Matcher;
import zeroecho.core.io.CancellationSignal;
import zeroecho.pki.application.PkiOperation;
import zeroecho.pki.application.PkiOperationFailure;
import zeroecho.pki.application.PkiOperationOutcome;
import zeroecho.pki.application.PkiOperationResult;
import zeroecho.pki.application.PkiOperationValue;
import zeroecho.pki.application.PkiSession;
/** Sequential shared executor used by direct commands and batch plans. */
final class PkiPlanExecutor {
private final PkiOperationRegistry registry;
/* default */ PkiPlanExecutor(PkiOperationRegistry registry) {
this.registry = Objects.requireNonNull(registry, "registry");
}
/* default */ PkiPlanExecution execute(PkiPlan plan, PkiSession session, CancellationSignal cancellation) {
List<PkiPlanExecution.StepResult> results = new ArrayList<>(plan.steps().size());
Map<String, PkiOperationResult> successful = new LinkedHashMap<>();
Map<String, PkiPlanExecution.StepStatus> states = new LinkedHashMap<>();
boolean failFast = false;
boolean cancelled = false;
for (PkiPlan.Step step : plan.steps()) {
if (failFast) {
addSkipped(results, states, step, "FAIL_FAST");
continue;
}
if (cancelled) {
addSkipped(results, states, step, "CANCELLED");
continue;
}
if (cancellation.isCancelled()) {
PkiPlanExecution.StepResult result = PkiPlanExecution.StepResult.failure(step.id(), step.operation(),
PkiOperationFailure.CANCELLED, "OPERATION_CANCELLED");
results.add(result);
states.put(step.id(), result.status());
cancelled = true;
continue;
}
Resolution resolution;
try {
resolution = resolve(step.arguments(), successful, states);
} catch (IllegalArgumentException failure) {
PkiPlanExecution.StepResult result = PkiPlanExecution.StepResult.failure(step.id(), step.operation(),
PkiOperationFailure.VALIDATION_FAILURE, "REFERENCE_FIELD_INVALID");
results.add(result);
states.put(step.id(), result.status());
if (plan.failurePolicy() == PkiPlan.FailurePolicy.FAIL_FAST) {
failFast = true;
}
continue;
}
if (resolution.dependencyUnavailable()) {
addSkipped(results, states, step, "DEPENDENCY_UNAVAILABLE");
continue;
}
PkiPlanExecution.StepResult result = executeStep(step, resolution.value(), plan, session, cancellation);
results.add(result);
states.put(step.id(), result.status());
result.output().ifPresent(value -> successful.put(step.id(), value));
if (result.failure().filter(value -> value == PkiOperationFailure.CANCELLED).isPresent()) {
cancelled = true;
}
if (result.status() == PkiPlanExecution.StepStatus.FAILED
&& plan.failurePolicy() == PkiPlan.FailurePolicy.FAIL_FAST) {
failFast = true;
}
}
return new PkiPlanExecution(results);
}
private PkiPlanExecution.StepResult executeStep(PkiPlan.Step step, PkiOperationValue value, PkiPlan plan,
PkiSession session, CancellationSignal cancellation) {
try {
PkiOperation operation = registry.bind(step.operation(), PkiCliConfiguration.object(value),
plan.baseDirectory());
PkiOperationOutcome outcome = session.operations().execute(operation, cancellation);
if (outcome instanceof PkiOperationOutcome.Success success) {
return PkiPlanExecution.StepResult.success(step.id(), step.operation(), success.result());
}
PkiOperationOutcome.Failure failure = (PkiOperationOutcome.Failure) outcome;
return PkiPlanExecution.StepResult.failure(step.id(), step.operation(), failure.classification(),
failure.code());
} catch (IllegalArgumentException failure) {
return PkiPlanExecution.StepResult.failure(step.id(), step.operation(),
PkiOperationFailure.VALIDATION_FAILURE, "OPERATION_ARGUMENTS_INVALID");
} catch (IOException failure) {
return PkiPlanExecution.StepResult.failure(step.id(), step.operation(),
PkiOperationFailure.RESOURCE_FAILURE, "OPERATION_INPUT_FAILED");
} catch (RuntimeException failure) { // NOPMD - application boundary redacts unexpected provider details.
return PkiPlanExecution.StepResult.failure(step.id(), step.operation(),
PkiOperationFailure.INTERNAL_FAILURE, "INTERNAL_FAILURE");
}
}
private static Resolution resolve(PkiOperationValue value, Map<String, PkiOperationResult> successful,
Map<String, PkiPlanExecution.StepStatus> states) {
if (value instanceof PkiOperationValue.Text text) {
Matcher matcher = PkiPlanParser.REFERENCE.matcher(text.value());
if (!matcher.matches()) {
return Resolution.available(value);
}
String stepId = matcher.group(1);
if (states.get(stepId) != PkiPlanExecution.StepStatus.SUCCEEDED) {
return Resolution.unavailable();
}
Optional<PkiOperationValue> field = successful.get(stepId).field(matcher.group(2));
if (field.isEmpty()) {
throw new IllegalArgumentException("Referenced output field does not exist");
}
return Resolution.available(field.orElseThrow());
}
if (value instanceof PkiOperationValue.ObjectValue object) {
Map<String, PkiOperationValue> fields = new LinkedHashMap<>();
for (Map.Entry<String, PkiOperationValue> entry : object.fields().entrySet()) {
Resolution child = resolve(entry.getValue(), successful, states);
if (child.dependencyUnavailable()) {
return Resolution.unavailable();
}
fields.put(entry.getKey(), child.value());
}
return Resolution.available(new PkiOperationValue.ObjectValue(fields));
}
if (value instanceof PkiOperationValue.ListValue list) {
List<PkiOperationValue> values = new ArrayList<>(list.values().size());
for (PkiOperationValue item : list.values()) {
Resolution child = resolve(item, successful, states);
if (child.dependencyUnavailable()) {
return Resolution.unavailable();
}
values.add(child.value());
}
return Resolution.available(new PkiOperationValue.ListValue(values));
}
return Resolution.available(value);
}
private static void addSkipped(List<PkiPlanExecution.StepResult> results,
Map<String, PkiPlanExecution.StepStatus> states, PkiPlan.Step step, String code) {
results.add(PkiPlanExecution.StepResult.skipped(step.id(), step.operation(), code));
states.put(step.id(), PkiPlanExecution.StepStatus.SKIPPED);
}
private record Resolution(PkiOperationValue value, boolean dependencyUnavailable) {
/* default */ static Resolution available(PkiOperationValue value) {
return new Resolution(value, false);
}
/* default */ static Resolution unavailable() {
return new Resolution(new PkiOperationValue.BooleanValue(false), true);
}
}
}

View File

@@ -0,0 +1,150 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.cli;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import zeroecho.pki.application.PkiOperationValue;
/** Strict parser and reference-order validator for plan schema version 1. */
final class PkiPlanParser {
/* default */ static final int MAXIMUM_STEPS = 1_000;
/* default */ static final Pattern REFERENCE = Pattern
.compile("\\$\\{([a-z][a-z0-9-]{0,63})\\.([a-z][a-zA-Z0-9]{0,63})}");
private static final Pattern STEP_ID = Pattern.compile("[a-z][a-z0-9-]{0,63}");
private static final Pattern OPERATION_NAME = Pattern.compile("[a-z][a-z0-9.-]{2,63}");
private static final Set<String> ROOT_FIELDS = Set.of("version", "failurePolicy", "operations");
private static final Set<String> STEP_FIELDS = Set.of("id", "operation", "arguments");
private PkiPlanParser() {
}
/* default */ static PkiPlan read(Path path, Set<String> supportedOperations) throws IOException {
Path exact = path.toAbsolutePath().normalize();
PkiOperationValue.ObjectValue root = PkiCliConfiguration
.object(PkiCliJson.parse(PkiCliFiles.readRegularFile(exact, PkiCliJson.MAXIMUM_DOCUMENT_BYTES)));
PkiCliConfiguration.requireExactFields(root.fields(), ROOT_FIELDS);
int version = Math.toIntExact(PkiCliConfiguration.integer(PkiCliConfiguration.required(root, "version")));
if (version != PkiPlan.CURRENT_VERSION) {
throw new IllegalArgumentException("Plan version is unsupported");
}
PkiPlan.FailurePolicy policy = parsePolicy(PkiCliConfiguration.text(
PkiCliConfiguration.required(root, "failurePolicy")));
List<PkiOperationValue> encodedSteps = list(PkiCliConfiguration.required(root, "operations"));
if (encodedSteps.isEmpty() || encodedSteps.size() > MAXIMUM_STEPS) {
throw new IllegalArgumentException("Plan step count is invalid");
}
List<PkiPlan.Step> steps = new ArrayList<>(encodedSteps.size());
Set<String> ids = new HashSet<>();
Map<String, Integer> positions = new HashMap<>();
for (int index = 0; index < encodedSteps.size(); index++) {
PkiPlan.Step step = parseStep(encodedSteps.get(index), supportedOperations);
if (!ids.add(step.id())) {
throw new IllegalArgumentException("Plan step identifiers must be unique");
}
positions.put(step.id(), index);
steps.add(step);
}
validateReferences(steps, positions);
Path parent = exact.getParent();
return new PkiPlan(version, policy, steps, parent == null ? Path.of(".") : parent);
}
private static PkiPlan.Step parseStep(PkiOperationValue value, Set<String> supportedOperations) {
PkiOperationValue.ObjectValue object = PkiCliConfiguration.object(value);
PkiCliConfiguration.requireExactFields(object.fields(), STEP_FIELDS);
String id = PkiCliConfiguration.text(PkiCliConfiguration.required(object, "id"));
String operation = PkiCliConfiguration.text(PkiCliConfiguration.required(object, "operation"));
if (!STEP_ID.matcher(id).matches() || !OPERATION_NAME.matcher(operation).matches()
|| !supportedOperations.contains(operation)) {
throw new IllegalArgumentException("Plan step identity or operation is invalid");
}
PkiOperationValue.ObjectValue arguments = PkiCliConfiguration
.object(PkiCliConfiguration.required(object, "arguments"));
return new PkiPlan.Step(id, operation, arguments);
}
private static void validateReferences(List<PkiPlan.Step> steps, Map<String, Integer> positions) {
for (int index = 0; index < steps.size(); index++) {
validateValueReferences(steps.get(index).arguments(), index, positions);
}
}
@SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
private static void validateValueReferences(PkiOperationValue value, int current,
Map<String, Integer> positions) {
if (value instanceof PkiOperationValue.Text text) {
String raw = text.value();
Matcher matcher = REFERENCE.matcher(raw);
if (matcher.matches()) {
Integer referenced = positions.get(matcher.group(1));
if (referenced == null || referenced >= current) {
throw new IllegalArgumentException("Plan reference must target an earlier step");
}
} else if (raw.contains("${")) {
throw new IllegalArgumentException("Plan interpolation is not supported");
}
} else if (value instanceof PkiOperationValue.ObjectValue object) {
for (PkiOperationValue child : object.fields().values()) {
validateValueReferences(child, current, positions);
}
} else if (value instanceof PkiOperationValue.ListValue list) {
for (PkiOperationValue child : list.values()) {
validateValueReferences(child, current, positions);
}
}
}
private static PkiPlan.FailurePolicy parsePolicy(String value) {
return PkiPlan.FailurePolicy.valueOf(value);
}
private static List<PkiOperationValue> list(PkiOperationValue value) {
if (value instanceof PkiOperationValue.ListValue list) {
return list.values();
}
throw new IllegalArgumentException("CLI field has the wrong type");
}
}

View File

@@ -0,0 +1,36 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/** Strict PKI command, plan, binding and output adapters owned by the application module. */
package zeroecho.pki.cli;