From e9e614a4644288148abe6cb6fae3112e25a86d2d Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 06 Oct 2026 06:58:49 +0000
Subject: [PATCH] [#1159] Keep administrator-typed DNs intact in dsconfig batch lines, tools.properties and the SDK LDAPUrl (#1166)
---
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java | 157 ++++++++++++++++++++++++++++++++++------------------
1 files changed, 102 insertions(+), 55 deletions(-)
diff --git a/opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java b/opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java
index 07d83f1..27b049f 100644
--- a/opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java
+++ b/opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java
@@ -1394,40 +1394,38 @@
try (BufferedReader bReader = batchCommandsReader()) {
List<String> initialArgs = removeBatchArgs(args);
- // Split the CLI string into arguments array
- String command = "";
- String line;
- while ((line = bReader.readLine()) != null) {
- if (line.isEmpty() || line.startsWith("#")) {
- // Empty line or comment
- continue;
- }
- // command split in several line support
- if (line.endsWith("\\")) {
- // command is split into several lines
- command += line.substring(0, line.length() - 1);
- continue;
- }
+ String command;
+ while ((command = nextBatchCommand(bReader)) != null) {
+ // Only the echo is trimmed: an escaped blank at the end of the command is kept.
+ printlnNoWrap(LocalizableMessage.raw(command.trim()));
- command += line;
- command = command.trim();
- printlnNoWrap(LocalizableMessage.raw(command));
-
- // Append initial arguments to the file line
- final String[] allArgsArray = buildCommandArgs(initialArgs, command);
- int exitCode = main(allArgsArray, getOutputStream(), getErrorStream());
+ final int exitCode = runBatchCommand(initialArgs, command, getOutputStream(), getErrorStream());
if (exitCode != ReturnCode.SUCCESS.get()) {
System.exit(filterExitCode(exitCode));
}
println();
- // reset command
- command = "";
}
} catch (IOException ex) {
errPrintln(ERR_DSCFG_ERROR_READING_BATCH_FILE.get(ex));
}
}
+ /**
+ * Runs one command of a batch with the initial arguments appended, and returns its exit code.
+ * A command that cannot be split into arguments is reported on the error stream: a batch
+ * always runs with --no-prompt, so that is where dsconfig writes its errors.
+ */
+ static int runBatchCommand(final List<String> initialArgs, final String command, final PrintStream out,
+ final PrintStream err) {
+ try {
+ // Append initial arguments to the file line
+ return main(buildCommandArgs(initialArgs, command), out, err);
+ } catch (final ArgumentException e) {
+ err.println(wrapText(e.getMessageObject(), MAX_LINE_WIDTH));
+ return ReturnCode.ERROR_USER_DATA.get();
+ }
+ }
+
private BufferedReader batchCommandsReader() throws FileNotFoundException {
if (batchArgument.isPresent()) {
return new BufferedReader(new InputStreamReader(System.in));
@@ -1440,7 +1438,38 @@
}
}
- private String[] buildCommandArgs(List<String> initialArgs, String batchCommand) {
+ /**
+ * Reads the next command of a batch, or returns {@code null} at its end. Lines that are empty
+ * or hold only blanks, and comments, are skipped, also inside a command that continues over
+ * several lines, and a line that ends in an unescaped backslash continues on the next line. A
+ * command whose last line continues still runs at the end of the batch.
+ */
+ static String nextBatchCommand(final BufferedReader reader) throws IOException {
+ final StringBuilder command = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.trim().isEmpty() || line.startsWith("#")) {
+ // Empty or blank line, or comment
+ continue;
+ }
+ if (!continuesOnNextLine(line)) {
+ return command.append(line).toString();
+ }
+ command.append(line, 0, line.length() - 1);
+ }
+ return command.toString().trim().isEmpty() ? null : command.toString();
+ }
+
+ /** Whether a line ends in a backslash that is not itself escaped by a backslash. */
+ private static boolean continuesOnNextLine(final String line) {
+ int backslashes = 0;
+ for (int i = line.length() - 1; i >= 0 && line.charAt(i) == '\\'; i--) {
+ backslashes++;
+ }
+ return backslashes % 2 == 1;
+ }
+
+ private static String[] buildCommandArgs(List<String> initialArgs, String batchCommand) throws ArgumentException {
final Collection<String> commandArgs = toCommandArgs(batchCommand);
final int length = commandArgs.size() + initialArgs.size();
final List<String> allArguments = new ArrayList<>(length);
@@ -1449,48 +1478,66 @@
return allArguments.toArray(new String[length]);
}
- static Collection<String> toCommandArgs(String command) {
- Collection<String> commandArgs = new ArrayList<>();
- StringBuilder builder = new StringBuilder();
- boolean inQuotes = false;
+ /**
+ * Splits a batch line into arguments the way a POSIX shell does, without any expansion: a
+ * backslash outside quotes escapes the next char, single quotes keep everything literally, and
+ * inside double quotes a backslash escapes only {@code "}, {@code \}, {@code $} and {@code `}.
+ *
+ * @throws ArgumentException
+ * If a quote is not closed.
+ */
+ static Collection<String> toCommandArgs(String command) throws ArgumentException {
+ final Collection<String> commandArgs = new ArrayList<>();
+ final StringBuilder builder = new StringBuilder();
+ // A word that holds only quotes is still an (empty) argument.
+ boolean inWord = false;
for (int i = 0; i < command.length(); i++) {
final char c = command.charAt(i);
switch (c) {
+ case ' ':
+ case '\t':
+ if (inWord) {
+ commandArgs.add(builder.toString());
+ builder.setLength(0);
+ inWord = false;
+ }
+ continue;
+ case '\\':
+ // A trailing backslash has nothing to escape and is kept.
+ builder.append(i + 1 < command.length() ? command.charAt(++i) : c);
+ break;
+ case '\'':
+ final int end = command.indexOf('\'', i + 1);
+ if (end < 0) {
+ throw new ArgumentException(ERR_DSCFG_ERROR_BATCH_UNCLOSED_QUOTE.get(command.trim()));
+ }
+ builder.append(command, i + 1, end);
+ i = end;
+ break;
+ case '"':
+ for (i++; i < command.length() && command.charAt(i) != '"'; i++) {
+ if (command.charAt(i) == '\\' && i + 1 < command.length()
+ && "\"\\$`".indexOf(command.charAt(i + 1)) >= 0) {
+ i++;
+ }
+ builder.append(command.charAt(i));
+ }
+ if (i == command.length()) {
+ throw new ArgumentException(ERR_DSCFG_ERROR_BATCH_UNCLOSED_QUOTE.get(command.trim()));
+ }
+ break;
default:
builder.append(c);
break;
- case '\\':
- builder.append(command.charAt(++i));
- break;
- case '"':
- if (inQuotes) {
- builder = newArgumentString(commandArgs, builder);
- inQuotes = false;
- } else {
- inQuotes = true;
- }
- break;
- case ' ':
- if (inQuotes) {
- builder.append(c);
- } else {
- builder = newArgumentString(commandArgs, builder);
- }
- break;
}
+ inWord = true;
}
- newArgumentString(commandArgs, builder);
+ if (inWord) {
+ commandArgs.add(builder.toString());
+ }
return commandArgs;
}
- private static StringBuilder newArgumentString(Collection<String> commandArgs, StringBuilder stringBuilder) {
- if (stringBuilder.length() > 0) {
- commandArgs.add(stringBuilder.toString());
- stringBuilder = new StringBuilder();
- }
- return stringBuilder;
- }
-
private List<String> removeBatchArgs(String[] args) {
// Build a list of initial arguments,
// removing the batch file option + its value
--
Gitblit v1.10.0