mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
2 days ago e9e614a4644288148abe6cb6fae3112e25a86d2d
opendj-config/src/test/java/org/forgerock/opendj/config/dsconfig/DSConfigParseTest.java
@@ -12,15 +12,26 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Portions copyright 2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.forgerock.opendj.config.dsconfig;
import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.ReturnCode;
import org.forgerock.testng.ForgeRockTestCase;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@Test(groups = { "precommit", "config" })
public class DSConfigParseTest extends ForgeRockTestCase {
@@ -45,4 +56,131 @@
        Collection<String> cmdLine = DSConfig.toCommandArgs(arg);
        Assert.assertEquals(cmdLine.iterator().next(), value);
    }
    /** Batch lines and the arguments a POSIX shell splits them into. */
    @DataProvider
    public Object[][] shellQuoting() {
        return new Object[][] {
            // Inside double quotes a backslash is kept unless it precedes ", \, $ or `.
            { "--set base-dn:\"cn=a\\,b,dc=x\"", args("--set", "base-dn:cn=a\\,b,dc=x") },
            { "--set \"base-dn:cn=a\\2Cb,dc=x\"", args("--set", "base-dn:cn=a\\2Cb,dc=x") },
            { "\"a\\\"b\\\\c\\$d\\`e\"", args("a\"b\\c$d`e") },
            // Single quotes keep everything literally, a backslash included.
            { "--set 'base-dn:o=My Company'", args("--set", "base-dn:o=My Company") },
            { "--set 'base-dn:cn=a\\,b,dc=x'", args("--set", "base-dn:cn=a\\,b,dc=x") },
            { "'say \"hi\"' \"it's\"", args("say \"hi\"", "it's") },
            // Quoted and unquoted parts of one word make a single argument.
            { "base-dn:\"o=My \"'Company'", args("base-dn:o=My Company") },
            { "\"a\"b c", args("ab", "c") },
            // An empty quoted word is an empty argument.
            { "--set description:\"\" \"\"", args("--set", "description:", "") },
            // Tabs separate arguments as spaces do.
            { "a\tb  c", args("a", "b", "c") },
            // A trailing backslash has nothing to escape and is kept.
            { "a b\\", args("a", "b\\") },
        };
    }
    @Test(dataProvider = "shellQuoting")
    public void testShellQuotingInCommandLine(String line, List<String> expected) throws Exception {
        Assert.assertEquals(new ArrayList<>(DSConfig.toCommandArgs(line)), expected);
    }
    /** Batch lines with a quote that is not closed: a POSIX shell rejects them. */
    @DataProvider
    public Object[][] unclosedQuotes() {
        return new Object[][] {
            // Before single quotes were supported, this line gave three arguments.
            { "--set description:O'Brien --advanced" },
            { "--set 'base-dn:o=My Company" },
            { "--set \"base-dn:o=My Company" },
            { "--set \"base-dn:o=My Company\\\"" },
        };
    }
    @Test(dataProvider = "unclosedQuotes", expectedExceptions = ArgumentException.class,
            expectedExceptionsMessageRegExp = "The following batch command has a quote that is not closed: .*")
    public void testUnclosedQuoteIsRejected(String line) throws Exception {
        DSConfig.toCommandArgs(line);
    }
    @Test
    public void testBatchCommandWithUnclosedQuoteFails() {
        final ByteArrayOutputStream err = new ByteArrayOutputStream();
        final int exitCode = DSConfig.runBatchCommand(args("--noPropertiesFile", "-n"), "set-x --set 'a",
                new PrintStream(new ByteArrayOutputStream()), new PrintStream(err));
        Assert.assertEquals(exitCode, ReturnCode.ERROR_USER_DATA.get());
        Assert.assertTrue(text(err).contains("The following batch command has a quote that is not closed: "
                + "set-x --set 'a"), text(err));
    }
    @Test
    public void testBatchCommandKeepsEscapedTrailingBlank() {
        // dsconfig fails on the first argument, before it reads --no-prompt, so it still writes
        // the error to the output stream.
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        final PrintStream stream = new PrintStream(output);
        // dsconfig has no such subcommand: the error quotes the argument it was given.
        final int exitCode = DSConfig.runBatchCommand(args("--noPropertiesFile", "-n"), "set-x\\ ", stream, stream);
        Assert.assertEquals(exitCode, ReturnCode.CONFLICTING_ARGS.get());
        Assert.assertTrue(text(output).contains("The provided argument \"set-x \" is not recognized"), text(output));
    }
    /** The output with every run of blanks and line breaks as a single space, as the console wraps lines. */
    private static String text(ByteArrayOutputStream out) {
        return new String(out.toByteArray()).replaceAll("\\s+", " ");
    }
    /** Batch files and the arguments of each command they hold. */
    @DataProvider
    public Object[][] batchFiles() {
        return new Object[][] {
            { "# comment\n\nset-x --foo bar\nset-y\n", commands(args("set-x", "--foo", "bar"), args("set-y")) },
            // A line of blanks is skipped as an empty line is.
            { "set-x\n   \nset-y\n", commands(args("set-x"), args("set-y")) },
            { "set-x\n \t\n", commands(args("set-x")) },
            // Empty lines and comments are skipped inside a continued command too, so a line of a
            // long command can be commented out.
            { "set-x \\\n#  --foo bar \\\n  --baz qux\n", commands(args("set-x", "--baz", "qux")) },
            { "set-x \\\n\n  --baz qux\n", commands(args("set-x", "--baz", "qux")) },
            // A line that ends in a backslash continues on the next line.
            { "set-x \\\n  --foo bar\n", commands(args("set-x", "--foo", "bar")) },
            { "set-x --foo ab\\\ncd\n", commands(args("set-x", "--foo", "abcd")) },
            // An escaped backslash at the end of a line does not continue it, as dsconfig
            // --commandFilePath writes a value that ends in a backslash on UNIX.
            { "set-x --bindPassword pa\\\\\nset-y --foo bar\n",
                commands(args("set-x", "--bindPassword", "pa\\"), args("set-y", "--foo", "bar")) },
            { "set-x --foo a\\\\\\\nb\n", commands(args("set-x", "--foo", "a\\b")) },
            // An escaped blank at the end of a line is kept.
            { "set-x --set description:value\\ \n", commands(args("set-x", "--set", "description:value ")) },
            // A command still runs when its last line continues at the end of the file.
            { "set-x --foo bar \\", commands(args("set-x", "--foo", "bar")) },
            { "set-x\n \\\n", commands(args("set-x")) },
        };
    }
    @Test(dataProvider = "batchFiles")
    public void testBatchFileCommands(String batch, List<List<String>> expected) throws Exception {
        final List<List<String>> actual = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new StringReader(batch))) {
            String command;
            while ((command = DSConfig.nextBatchCommand(reader)) != null) {
                actual.add(new ArrayList<>(DSConfig.toCommandArgs(command)));
            }
        }
        Assert.assertEquals(actual, expected);
    }
    private static List<String> args(String... args) {
        return Arrays.asList(args);
    }
    @SafeVarargs
    private static List<List<String>> commands(List<String>... commands) {
        return Arrays.asList(commands);
    }
}