/******************************************************************************* * 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 ROOT_FIELDS = Set.of("version", "failurePolicy", "operations"); private static final Set STEP_FIELDS = Set.of("id", "operation", "arguments"); private PkiPlanParser() { } /* default */ static PkiPlan read(Path path, Set 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 encodedSteps = list(PkiCliConfiguration.required(root, "operations")); if (encodedSteps.isEmpty() || encodedSteps.size() > MAXIMUM_STEPS) { throw new IllegalArgumentException("Plan step count is invalid"); } List steps = new ArrayList<>(encodedSteps.size()); Set ids = new HashSet<>(); Map 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 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 steps, Map 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 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 list(PkiOperationValue value) { if (value instanceof PkiOperationValue.ListValue list) { return list.values(); } throw new IllegalArgumentException("CLI field has the wrong type"); } }